Merge remote-tracking branch 'origin/sub_laptop_3' into main_laptop_1
This commit is contained in:
@@ -218,14 +218,14 @@ async def send_initial_analysis_complete_email(
|
||||
{design_rows}
|
||||
</div>
|
||||
<div class="notice">
|
||||
기본값으로 계산한 결과입니다. 종단설계 화면에서 검토·수정한 뒤 횡단설계에서
|
||||
기본값으로 계산한 결과입니다. 노선설계 화면에서 검토·수정한 뒤 횡단설계에서
|
||||
[확정]을 누르면 다음 단계(수량 산출)로 넘어갑니다.
|
||||
</div>
|
||||
"""
|
||||
else:
|
||||
design_block = """
|
||||
<div class="notice">
|
||||
초기 노선·종횡단 자동 계산은 완료되지 않았습니다. 종단설계 화면에서 직접
|
||||
초기 노선·종횡단 자동 계산은 완료되지 않았습니다. 노선설계 화면에서 직접
|
||||
경로를 계산해 주세요.
|
||||
</div>
|
||||
"""
|
||||
@@ -239,7 +239,7 @@ async def send_initial_analysis_complete_email(
|
||||
</div>
|
||||
{design_block}
|
||||
<p style="text-align: center; margin: 26px 0;">
|
||||
<a class="button" href="{html.escape(result_url)}">종단설계 화면 열기</a>
|
||||
<a class="button" href="{html.escape(result_url)}">노선설계 화면 열기</a>
|
||||
</p>
|
||||
<p style="color:#6b7280;font-size:13px;">
|
||||
로그인이 필요한 경우 인증 후 같은 화면으로 이동합니다.
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* (`_Engine_Cad_Table.cross_note_cad_entities`).
|
||||
* 처음 열 때 칸이 없으면 — 옛 글(`structure_note`)이 있으면 줄마다 Text 로 옮김 · 없으면 미리 넣음
|
||||
* (265-2 · 관 주인 측점 = 원형 마크 · 그 측점 구조물마다 지정된 구조물 조합 이름 줄 · 미지정 =
|
||||
* 「이름 조합 미지정」 · 이어서 좌측 패널 옵션 줄 = `structureOptionLines` · 166-1) · 그 밖 = 빈 도면.
|
||||
* 종류 이름만(같은 측점 같은 종류 한 줄) · 이어서 좌측 패널 옵션 줄 = `structureOptionLines` · 166-1) · 그 밖 = 빈 도면.
|
||||
* ⚠ 짝(①) = `B07_DesignDetail/B07_DesignDetail_Engine_Cad_CulvertNote.py` — `noteEntities` ·
|
||||
* `structureNoteLines` · `stationNoLabel` 와 같은 입력 → 같은 엔티티(id 만 다름) · 모달을 안 연
|
||||
* 측점은 B07 이 그것으로 그림 · 거울 시험 `test_260_note_cad.py` · `test_265_note_cad.py`.
|
||||
@@ -158,10 +158,11 @@ export const thingsAt = (things: readonly NoteThing[], chainageM: number): NoteT
|
||||
|
||||
/**
|
||||
* 구조물 조합 줄 — 구조물마다 지정된 조합(본체 → 그룹 차례) 이름 한 줄씩(이름 = 일위대가 이름 +
|
||||
* 조합 경로 옵션 값) · 같은 구조물의 같은 이름은 한 줄 · 미지정 = 「{구조물 이름} 조합 미지정」 ·
|
||||
* 조합 경로 옵션 값) · 같은 구조물의 같은 이름은 한 줄 · 미지정 = 종류 이름만(같은 측점 같은 종류는 한 줄) ·
|
||||
* 배관은 본체 줄 없음(원형 마크) — 보호공 · 흙막이 · 도수로 조합만 · 이어서 그 구조물 좌측 패널 옵션 줄(166-1).
|
||||
*/
|
||||
export function structureNoteLines(things: readonly NoteThing[], chainageM: number): string[] {
|
||||
const kinds = new Set<string>();
|
||||
return thingsAt(things, chainageM).flatMap((thing) => {
|
||||
const own = readSummaryItem(thing.summary_item);
|
||||
const slots: Array<[string, SummaryItem]> = Object.entries(readGroupItems(thing.group_items));
|
||||
@@ -171,9 +172,10 @@ export function structureNoteLines(things: readonly NoteThing[], chainageM: numb
|
||||
.map(([, item]) => summaryLabel(item));
|
||||
const combos = slots.length
|
||||
? [...new Set(names)]
|
||||
: thing.pipe
|
||||
: thing.pipe || kinds.has(thing.name)
|
||||
? []
|
||||
: [`${thing.name} 조합 미지정`];
|
||||
: [thing.name];
|
||||
if (!slots.length) kinds.add(thing.name);
|
||||
return [...combos, ...(thing.optionLines ?? [])];
|
||||
});
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ B06 [상세] 모달을 한 번도 안 연 측점(칸 = 없음)에 B07 이 같은
|
||||
(빈 엔티티)은 덮지 않음 · 옛 글(`structure_note`)이 있으면 옛 글이 먼저
|
||||
(모달이 처음 열 때 옮기는 차례와 같음).
|
||||
그림 = 관 주인 측점 원형 마크(배수관 · Φ · L 정수) + 측점 · 그 측점 구조물마다 지정된
|
||||
구조물 조합 이름 줄(미지정 = 「이름 조합 미지정」) + 그 구조물 좌측 패널 옵션 줄(166-1).
|
||||
구조물 조합 이름 줄(미지정 = 종류 이름만) + 그 구조물 좌측 패널 옵션 줄(166-1).
|
||||
옵션 줄은 파이썬으로 다시 짜지 않음 — 화면과 같은 TS(`structureOptionLines`)를 Node
|
||||
진입점(`B07_DesignDetail_Note_Options_Node.ts` · CLAUDE.md 5장 ②)으로 돌려 받음 ·
|
||||
실패는 비치명(조합 줄만).
|
||||
@@ -109,10 +109,11 @@ def things_at(things: list[Thing], chainage_m: float) -> list[Thing]:
|
||||
def structure_note_lines(things: list[Thing], chainage_m: float) -> list[str]:
|
||||
"""구조물 조합 줄 — TS `structureNoteLines` 짝.
|
||||
|
||||
본체 → 그룹 차례 · 같은 이름 한 줄 · 미지정 = 「이름 조합 미지정」 · 배관 본체 뺌 ·
|
||||
본체 → 그룹 차례 · 같은 이름 한 줄 · 미지정 = 종류 이름만(같은 측점 같은 종류 한 줄) · 배관 본체 뺌 ·
|
||||
이어서 그 구조물 좌측 패널 옵션 줄(`option_lines` · 166-1).
|
||||
"""
|
||||
lines: list[str] = []
|
||||
kinds: set[str] = set()
|
||||
for thing in things_at(things, chainage_m):
|
||||
own = _summary_item(thing.get("summary_item"))
|
||||
slots = ([("", own)] if own else []) + _group_items(thing.get("group_items"))
|
||||
@@ -123,8 +124,9 @@ def structure_note_lines(things: list[Thing], chainage_m: float) -> list[str]:
|
||||
]
|
||||
if slots:
|
||||
lines += list(dict.fromkeys(names))
|
||||
elif not thing["pipe"]:
|
||||
lines.append(f"{thing['name']} 조합 미지정")
|
||||
elif not thing["pipe"] and thing["name"] not in kinds:
|
||||
kinds.add(thing["name"])
|
||||
lines.append(thing["name"])
|
||||
lines += [str(line) for line in thing.get("option_lines") or []]
|
||||
return lines
|
||||
|
||||
|
||||
@@ -326,6 +326,14 @@ def refuse_used(folder: Path, kind: str, key: str) -> None:
|
||||
|
||||
|
||||
# ── 층 사이 복사 ──────────────────────────────────────────────────────
|
||||
def origin_mark(doc: dict[str, Any], ref: Ref) -> dict[str, Any]:
|
||||
"""복사 문서의 `본뜬` — 실무자료 본뜬(키 · 전국자재 …)은 지키고 원본 참조는 `복사원본` 칸에 둠."""
|
||||
own = doc.get("본뜬")
|
||||
if isinstance(own, dict) and own.get("층") == "실무자료":
|
||||
return {**doc, "본뜬": {**own, "복사원본": ref}}
|
||||
return {**doc, "본뜬": ref}
|
||||
|
||||
|
||||
def carry(doc: dict[str, Any], source: Here) -> dict[str, Any]:
|
||||
"""복사(원본 층 `source` → 지금 층) — 지금 층이 못 읽는 참조는 그 문서를 지금 층에 새 키로
|
||||
같이 복사(`본뜬` = 원래 참조) · 참조를 새 키로 바꿈. 원본 층 읽기 권한은 부르는 쪽 몫."""
|
||||
@@ -341,7 +349,7 @@ def carry(doc: dict[str, Any], source: Here) -> dict[str, Any]:
|
||||
got = read_ref(ref)
|
||||
if got is None:
|
||||
raise _bad(f"없는 참조 「{ref['키']}」")
|
||||
body = {**got, "담은로직": _moved(got.get("담은로직"), move, depth + 1), "본뜬": ref}
|
||||
body = origin_mark({**got, "담은로직": _moved(got.get("담은로직"), move, depth + 1)}, ref)
|
||||
made = create(kind_of(ref["키"]), body)
|
||||
done[ref_id(ref)] = {"층": target[0], "주인": target[1], "키": made["이름"]}
|
||||
return done[ref_id(ref)]
|
||||
|
||||
@@ -97,7 +97,7 @@ class LibraryBody(BaseModel):
|
||||
|
||||
|
||||
def _copy(kind: str, doc: dict[str, Any], source: refs.Here, ref: dict) -> dict[str, Any]:
|
||||
return refs.create(kind, refs.carry({**doc, "본뜬": ref}, source))
|
||||
return refs.create(kind, refs.carry(refs.origin_mark(doc, ref), source))
|
||||
|
||||
|
||||
@router.post("/layers/{layer}/library/{kind}")
|
||||
|
||||
@@ -127,9 +127,9 @@ export function buildOrder(ones: { 번: number; 아래: number[] }[]): number[]
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 단가산출 행 → 행 전체 범위 A1(`4:9`) */
|
||||
/** 단가산출 행 → B ~ F 열 범위 A1(`B4:F9`) */
|
||||
export const rowsText = (행: number | [number, number]): string =>
|
||||
typeof 행 === "number" ? `${행}:${행}` : `${행[0]}:${행[1]}`;
|
||||
typeof 행 === "number" ? `B${행}:F${행}` : `B${행[0]}:F${행[1]}`;
|
||||
|
||||
export function openHopyoSheet(opt: HopyoSheetOptions): void {
|
||||
let sheet: SpreadsheetHandle | null = null;
|
||||
|
||||
@@ -111,12 +111,15 @@ export function buildHopyoSection(opt: HopyoSectionOptions): HopyoSection {
|
||||
scope,
|
||||
key: file.키,
|
||||
파일: file.파일,
|
||||
canWrite: opt.canWrite,
|
||||
// 사전조합은 어느 화면 층에서든 로그인한 사람 개인 층에 만듦 — 회사 · 마스터는 구조물 카드 [가져오기]
|
||||
target: { layer: "personal", owner: null },
|
||||
canWrite: true,
|
||||
onClose: () => {
|
||||
if (sheetKey === file.키) sheetKey = null;
|
||||
paintList();
|
||||
},
|
||||
onDeleted: () => void refresh(),
|
||||
// [삭제]는 실무자료 파일이 있는 화면 층 쓰기 권한 때만
|
||||
onDeleted: opt.canWrite ? () => void refresh() : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# 종단설계 설명서
|
||||
# 노선설계 설명서
|
||||
|
||||
## 이 화면에서 하는 일
|
||||
|
||||
|
||||
@@ -125,7 +125,7 @@ const LOCALES = {
|
||||
B06_Profile_Btn_Save: ["저장", "Save"],
|
||||
B06_Profile_Btn_Confirm: ["확정", "Confirm"],
|
||||
Design_Btn_ToCross: ["횡단 보기", "Cross view"],
|
||||
Design_Btn_ToProfile: ["종단 보기", "Profile view"],
|
||||
Design_Btn_ToProfile: ["노선 보기", "Route view"],
|
||||
};
|
||||
const STUBS = {
|
||||
"@ui/ui_template_locale": { ui_locales: LOCALES, currentLanguageIndex: 0 },
|
||||
|
||||
@@ -107,7 +107,7 @@ def test_조합_줄_뒤에_옵션_줄_미지정도_옵션_줄():
|
||||
"group_items": None,
|
||||
"option_lines": ["옹벽: 높이 2.5m"],
|
||||
}
|
||||
assert structure_note_lines([thing], 40.0) == ["옹벽 조합 미지정", "옹벽: 높이 2.5m"]
|
||||
assert structure_note_lines([thing], 40.0) == ["옹벽", "옹벽: 높이 2.5m"]
|
||||
|
||||
|
||||
def test_화면은_구조물마다_옵션_줄을_붙임():
|
||||
|
||||
@@ -124,12 +124,12 @@ _LINES = [
|
||||
"기슭막이 메쌓기 1.2m 기초무",
|
||||
"돌붙임 찰붙임",
|
||||
"옹벽 콘크리트 H=2.0",
|
||||
"돌쌓기(찰) 조합 미지정",
|
||||
"가드레일 조합 미지정",
|
||||
"돌쌓기(찰)",
|
||||
"가드레일",
|
||||
]
|
||||
|
||||
|
||||
def test_구조물_여럿_글은_조합_이름_미지정_배관_본체_뺌_TS_파이썬_같음():
|
||||
def test_구조물_여럿_글은_조합_이름_미지정은_종류_이름만_배관_본체_뺌_TS_파이썬_같음():
|
||||
culvert = {**_culvert_set(_PIPE), "chainage_m": 40.2}
|
||||
cases = [
|
||||
_case(_THINGS, 40.0, culvert, {"pipe_length_m": 8.5}),
|
||||
@@ -229,3 +229,15 @@ def test_원형_마크_L_은_사사오입_정수_TS_파이썬_같음(length, lab
|
||||
py = note_entities(40.0, culvert, design, "NO.2", [])
|
||||
assert _labels(ts["entities"])[2] == label
|
||||
assert _labels(py)[2] == label
|
||||
|
||||
|
||||
def test_미지정_같은_종류는_한_줄_조합_있는_같은_이름은_따로_TS_파이썬_같음():
|
||||
things = [
|
||||
{"name": "옹벽", "pipe": False, "at": 40.0},
|
||||
{"name": "옹벽", "pipe": False, "at": 40.1},
|
||||
{"name": "돌쌓기", "pipe": False, "at": 40.0, "summary_item": _B},
|
||||
{"name": "돌쌓기", "pipe": False, "at": 40.0},
|
||||
]
|
||||
[ts] = _helper("mirror", [_case(things, 40.0, None, {})])
|
||||
lines = structure_note_lines(things, 40.0)
|
||||
assert ts["lines"] == lines == ["옹벽", "돌붙임 찰붙임", "돌쌓기"]
|
||||
|
||||
@@ -95,7 +95,7 @@ def test_아래_단추는_네_개_보기_전환_글자만_따라감(results):
|
||||
# [확정]은 종단 보기에서도 켜진다(한 벌 저장이 횡단 자료를 같은 페이지에서 얻음).
|
||||
assert views[name]["dockDisabled"] == [False, False, False, False]
|
||||
assert views["profile"]["dockLabels"][3] == "횡단 보기"
|
||||
assert views["cross"]["dockLabels"][3] == "종단 보기"
|
||||
assert views["cross"]["dockLabels"][3] == "노선 보기"
|
||||
assert views["back"]["dockLabels"][3] == "횡단 보기"
|
||||
|
||||
|
||||
|
||||
@@ -287,7 +287,7 @@ def test_엑셀_보기_창_행과_호표():
|
||||
assert sheet["at"] == [None, 3, 3, 3, 4, 4, None] # 22행 → 제3호표 · 범위 밖 = 없음
|
||||
assert sheet["span"] == "B20:O24" # 제3호표 영역 = B열 ~ 끝 열(열 폭 정보 O)
|
||||
assert sheet["last"] == ["AA", "A"]
|
||||
assert sheet["rows"] == ["4345:4345", "93:101"] # 단가산출 행 전체
|
||||
assert sheet["rows"] == ["B4345:F4345", "B93:F101"] # 단가산출 행 B~F 열
|
||||
|
||||
|
||||
# ── 소스 ──────────────────────────────────────────────────────────────
|
||||
@@ -348,7 +348,10 @@ def test_좌측_판_자리와_권한():
|
||||
assert "size:" not in up and "엑셀 올리기" not in hopyo_side
|
||||
assert "buildHopyoView" not in hopyo_side
|
||||
assert 'row.addEventListener("click", () => void showSheet(f));' in hopyo_side
|
||||
assert "onClose:" in hopyo_side and "onDeleted: () => void refresh()," in hopyo_side
|
||||
assert (
|
||||
"onClose:" in hopyo_side
|
||||
and "onDeleted: opt.canWrite ? () => void refresh() : undefined," in hopyo_side
|
||||
)
|
||||
assert 'accept: ".xls,.xlsx"' in hopyo_side and "input.hidden = true;" in hopyo_side
|
||||
assert "disabled: !opt.canWrite," in hopyo_side and "pageScope()" in hopyo_side
|
||||
assert "buildHopyoLedger({ scope, canWrite: opt.canWrite })" in hopyo_side # [별칭 장부]
|
||||
@@ -524,3 +527,11 @@ def test_명사형_단추와_별칭_장부_풀이_359():
|
||||
'label: "지우기"',
|
||||
):
|
||||
assert gone not in every, gone
|
||||
|
||||
|
||||
def test_372_2_저장은_개인_층에_삭제는_화면_층_권한():
|
||||
side = _text("M02_MasterTemplete_Hopyo_Side.ts")
|
||||
open_ = side.split("sheet.openHopyoSheet({")[1].split("});")[0]
|
||||
assert 'target: { layer: "personal", owner: null },' in open_
|
||||
assert "canWrite: true," in open_ # 만들기 = 개인 층 · 화면 층 무관
|
||||
assert "onDeleted: opt.canWrite ? () => void refresh() : undefined," in open_
|
||||
|
||||
@@ -53,7 +53,7 @@ def test_실제_응답_화면_도우미(client):
|
||||
assert states == {"맞음": 122, "다른 점": 86, "후보": 60} # 268 줄 · 셈 끝
|
||||
assert got["count"]["top"] == 9 # 「후보」 줄 1순위 근거 맞음(근거 맞는 줄은 거의 숫자로 풀림)
|
||||
assert got["at"] == [None, 3, 3, 3, None, 4] # 25행 = 호표 사이 빈 줄
|
||||
assert got["span"] == "B20:O24" and got["rows"] == "4229:4244"
|
||||
assert got["span"] == "B20:O24" and got["rows"] == "B4229:F4244"
|
||||
by = {x["이름"]: x for x in got["lines"]}
|
||||
dig = by["구조물터파기(인력100%)"]
|
||||
assert dig["source"] == "출처 건설품셈 3-2-1 · 직종 특별인부 · 단가산출 4229–4244행"
|
||||
|
||||
@@ -120,3 +120,15 @@ def test_확인_창_세_칸_만들기_값(client):
|
||||
assert seoul["대상"][0]["세칸"]["자재구분"] == "사급" # 고른 자재 이름(-사급) 먼저
|
||||
rows = (M02 / "M02_MasterTemplete_Hopyo_CheckRows.ts").read_text(encoding="utf-8")
|
||||
assert "...markCells(p?.세칸 ??" in rows
|
||||
|
||||
|
||||
def test_복사_때_실무자료_본뜬과_전국자재_지킴():
|
||||
from M02_MasterTemplete import M02_Labor_Ref as refs
|
||||
|
||||
ref = {"층": "personal", "주인": "7/42", "키": "SC-000001"}
|
||||
mine = {"층": "실무자료", "키": "HP-000001 제1호표", "전국자재": [REMICON]}
|
||||
kept = refs.origin_mark({"본뜬": mine}, ref)["본뜬"]
|
||||
assert kept["키"] == "HP-000001 제1호표" and kept["전국자재"] == [REMICON] # 덮지 않음
|
||||
assert kept["복사원본"] == ref # 원본 참조는 다른 칸
|
||||
assert refs.origin_mark({"본뜬": None}, ref)["본뜬"] == ref # 보통 문서 = 옛 그대로
|
||||
assert refs.origin_mark({"본뜬": {"층": "system", "키": "A"}}, ref)["본뜬"] == ref
|
||||
|
||||
@@ -108,7 +108,7 @@ export const ui_locales_b2 = {
|
||||
],
|
||||
|
||||
/* --- B05_Profile 경로 설계 --- */
|
||||
B05_Route_Title: ["종단설계", "Profile Design"],
|
||||
B05_Route_Title: ["노선설계", "Route Design"],
|
||||
B05_Route_Group_Points: ["경로 제어점", "Route Control Points"],
|
||||
B05_Route_Point_BP: ["시점 (BP)", "Begin (BP)"],
|
||||
B05_Route_Point_EP: ["종점 (EP)", "End (EP)"],
|
||||
@@ -146,7 +146,7 @@ export const ui_locales_b2 = {
|
||||
B05_Route_Btn_GoCross: ["횡단 이동", "Go to Cross"],
|
||||
/* 좌측 패널 아래 단추 [보기 전환] — 가려는 쪽 이름(계획서 24-2b). */
|
||||
Design_Btn_ToCross: ["횡단 보기", "Cross view"],
|
||||
Design_Btn_ToProfile: ["종단 보기", "Profile view"],
|
||||
Design_Btn_ToProfile: ["노선 보기", "Route view"],
|
||||
B05_Route_TempSave_Success: ["변경 내용을 저장했습니다.", "Changes saved."],
|
||||
B05_Route_TempSave_Failed: ["저장에 실패했습니다.", "Failed to save."],
|
||||
B05_Route_Reset_Confirm: [
|
||||
@@ -800,7 +800,7 @@ export const ui_locales_b2 = {
|
||||
"Failed to load the material summary.",
|
||||
],
|
||||
B08_Quantity_Haul_Missing: [
|
||||
"운반계획이 아직 없습니다. 종단설계에서 [확정]을 누르면 만들어집니다.",
|
||||
"운반계획이 아직 없습니다. 노선설계에서 [확정]을 누르면 만들어집니다.",
|
||||
"No haul plan yet. Press [Confirm] on the profile design to build it.",
|
||||
],
|
||||
B08_Quantity_Haul_Excluded: ["내역 제외", "Not billed"],
|
||||
|
||||
@@ -80,7 +80,7 @@ export const ui_locales_common = {
|
||||
* 워크플로우 공통 (B04~B09 상단 진행 단계 라벨)
|
||||
* ------------------------------------------------------------------------ */
|
||||
WF_Step_Surface: ["전처리", "Preprocess"],
|
||||
WF_Step_Route: ["종단설계", "Profile Design"],
|
||||
WF_Step_Route: ["노선설계", "Route Design"],
|
||||
WF_Step_ProfileCross: ["횡단설계", "Cross Design"],
|
||||
WF_Step_DesignDetail: ["상세설계", "Detail Design"],
|
||||
WF_Step_Quantity: ["수량산출", "Quantity"],
|
||||
|
||||
Reference in New Issue
Block a user