Merge remote-tracking branches 'origin/sub_laptop_1' and 'origin/sub_laptop_3' into main_laptop_1
This commit is contained in:
@@ -217,6 +217,18 @@
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* 255-3 칸 이름 = 내용 글 크기 이상 · 한 줄 · 소재지(주소 · 지도) 위 가로선 */
|
||||
.b08-milestone__modal .ui-field__label {
|
||||
font-size: var(--text-body-sm);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.b08-milestone__place-field {
|
||||
margin-top: var(--spacing-8);
|
||||
padding-top: var(--spacing-16);
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
/* 연결선 누름 자리 — 투명한 굵은 선(244-13) */
|
||||
.b08-milestone__line-hit {
|
||||
stroke: transparent;
|
||||
|
||||
@@ -37,6 +37,7 @@ import { request } from "./B08_Quantity_UI_Hydro";
|
||||
import { editMap, type MapPin } from "./B08_Quantity_UI_Milestone_KakaoMap";
|
||||
import { placePicker } from "./B08_Quantity_UI_Milestone_Place";
|
||||
import {
|
||||
capturePlace,
|
||||
findSegmentKm,
|
||||
liveMilestone,
|
||||
putMilestone,
|
||||
@@ -58,8 +59,6 @@ export const ITEMS: Record<"route" | "base", string[]> = {
|
||||
const line = (...children: HTMLElement[]): HTMLElement =>
|
||||
el("div", { className: "b08-milestone__line-edit", children });
|
||||
|
||||
const head = (text: string): HTMLElement => el("h4", { text });
|
||||
|
||||
/** 길찾기 결과 한 줄 → 알림(까닭 줄 = 경고) */
|
||||
export function toastKm(text: string): void {
|
||||
if (text) showToast(`길찾기 — ${text}`, text.includes(" — ") ? "warning" : "success");
|
||||
@@ -79,7 +78,8 @@ function rowField(r: MilestoneRow, kind: "route" | "base") {
|
||||
list.replaceChildren(...ITEMS[k].map((t) => el("option", { attrs: { value: t } })));
|
||||
item.input.placeholder = `예 ${ITEMS[k][0]}`;
|
||||
// 기준점 = 거쳐 가는 자리 — 항목 · 목적지 없음(149)
|
||||
item.root.style.visibility = dest.root.style.visibility = k === "base" ? "hidden" : "";
|
||||
// 255-3 자리도 비움 — 기준점 이름이 분류 옆 첫 줄(두 줄로 안 꺾임)
|
||||
item.root.style.display = dest.root.style.display = k === "base" ? "none" : "";
|
||||
};
|
||||
setKind(kind);
|
||||
dest.select.value = r.dest;
|
||||
@@ -160,16 +160,18 @@ export function openPlaceModal(
|
||||
const where = place.value();
|
||||
const fresh = liveMilestone(projectId);
|
||||
let segment: string | undefined; // [등록] · 소재지 옮김 뒤 그 구간 길찾기 한 번
|
||||
let placed: string | undefined; // 256-1 등록 · 좌표 바뀜 = 장소 그림 캡처(뒤에서)
|
||||
if (kind === "base" && base) {
|
||||
const b = fresh.bases.find((x) => x.id === base.id);
|
||||
if (!b) return close();
|
||||
Object.assign(b, where, { name: text, dir: null });
|
||||
putMilestone(projectId, fresh);
|
||||
if (moved(where)) segment = b.id;
|
||||
if (moved(where)) segment = placed = b.id;
|
||||
} else if (kind === "base") {
|
||||
// 254 새 기준점 = 상자만 · 선은 연결 점 ● 로 이음(이을 때 길찾기)
|
||||
placed = `b${Date.now().toString(36)}${Math.floor(Math.random() * 1296).toString(36)}`;
|
||||
fresh.bases.push({
|
||||
id: `b${Date.now().toString(36)}${Math.floor(Math.random() * 1296).toString(36)}`,
|
||||
id: placed,
|
||||
name: text,
|
||||
dir: null,
|
||||
legs: [{ road: null, km: null }],
|
||||
@@ -186,11 +188,11 @@ export function openPlaceModal(
|
||||
rows: rowValues(),
|
||||
});
|
||||
putMilestone(projectId, fresh);
|
||||
if (moved(where)) segment = r.id;
|
||||
if (moved(where)) segment = placed = r.id;
|
||||
} else {
|
||||
if (!text) return name.setError("거래처 이름을 넣으세요.");
|
||||
// 155 등록 = 상자만 · 선은 다이어그램 연결 점 ● 로 이음(이을 때 길찾기)
|
||||
addRoute?.({
|
||||
placed = addRoute?.({
|
||||
name: text,
|
||||
to: "site",
|
||||
linked: false,
|
||||
@@ -201,6 +203,12 @@ export function openPlaceModal(
|
||||
}
|
||||
close();
|
||||
if (segment) void findSegmentKm(projectId, segment).then(toastKm);
|
||||
if (placed && where.lon != null && where.lat != null)
|
||||
void capturePlace(projectId, placed, {
|
||||
name: text || (kind === "base" ? "기준점" : "거래처"),
|
||||
lon: where.lon,
|
||||
lat: where.lat,
|
||||
});
|
||||
},
|
||||
});
|
||||
ok.dataset.b08MilestoneInfo = "save";
|
||||
@@ -231,7 +239,6 @@ export function openPlaceModal(
|
||||
className: "b08-milestone__grid",
|
||||
children: [sort.root, first.item.root, name.root, first.dest.root],
|
||||
}),
|
||||
head("소재지 — 주소 찾기 · 지도에서 고르기(방위는 이 자리로 자동)"),
|
||||
place.root,
|
||||
);
|
||||
},
|
||||
|
||||
@@ -20,6 +20,7 @@ import { chainOf, ownerOf } from "./B08_Quantity_Milestone_Layout";
|
||||
import { editMap, type MapPin } from "./B08_Quantity_UI_Milestone_KakaoMap";
|
||||
import { reverseAddress } from "./B08_Quantity_UI_Milestone_Place";
|
||||
import {
|
||||
capturePlace,
|
||||
deleteImage,
|
||||
findSegmentKm,
|
||||
hasUserImage,
|
||||
@@ -165,6 +166,8 @@ export function routeView(projectId: string, routeId: string): MilestoneView {
|
||||
const target = ownerOf(fresh, key);
|
||||
if (!target) return;
|
||||
Object.assign(target, { lon, lat, address });
|
||||
const label = fresh.bases.some((b) => b.id === key) ? "기준점" : "거래처";
|
||||
void capturePlace(projectId, key, { name: target.name || label, lon, lat }); // 256-1 장소 그림
|
||||
}
|
||||
putMilestone(projectId, fresh);
|
||||
await refind(touchingSegments(fresh, key));
|
||||
|
||||
@@ -130,9 +130,10 @@
|
||||
--pick-r: inset -2px 0 0 var(--color-danger);
|
||||
}
|
||||
|
||||
/* 183-3 자재단가대비표 — 적용 단가를 낸 출처 단가 칸(고른 출처 · 기본 최저) */
|
||||
/* 183-3 자재단가대비표 — 적용 단가를 낸 출처 단가 칸(고른 출처 · 기본 최저) · 257-3 초록 테두리
|
||||
* (바탕 색 아님 — 입력 칸 테두리(outline)와 달리 안쪽 상자 그림자) */
|
||||
.b09-estimation .ui-data-table__table tbody td.is-picked {
|
||||
background: color-mix(in srgb, var(--color-accent) 22%, var(--color-canvas));
|
||||
box-shadow: inset 0 0 0 2px var(--color-success);
|
||||
font-weight: var(--font-weight-bold);
|
||||
}
|
||||
|
||||
|
||||
@@ -246,6 +246,17 @@ export function renderB09Estimation(root: HTMLElement): void {
|
||||
const box = cell.getBoundingClientRect();
|
||||
openMenuAt(box.left, box.bottom, items);
|
||||
});
|
||||
// 257-3 칸 우클릭 메뉴 — 누른 칸 자리에 B09 공용 메뉴(줄 메뉴가 없는 표)
|
||||
if (spec.cellMenu)
|
||||
made.root.addEventListener("contextmenu", (event) => {
|
||||
const cell = (event.target as Element).closest<HTMLTableCellElement>("tr[data-i] td");
|
||||
const column = cell ? spec.columns[cellColumn(cell)] : undefined;
|
||||
const items =
|
||||
cell && column && spec.cellMenu?.(Number(cell.parentElement?.dataset.i), column.key);
|
||||
if (!items?.length) return;
|
||||
event.preventDefault();
|
||||
openMenuAt(event.clientX, event.clientY, items);
|
||||
});
|
||||
// 157-3 칸 풍선 — 공용 표는 그대로 · 올린 칸에 공용 호버 정보(185-3)
|
||||
if (spec.tip)
|
||||
attachHoverInfo(
|
||||
|
||||
@@ -111,6 +111,8 @@ export interface TabTable {
|
||||
cellClass?: (index: number, key: string) => string | null | undefined;
|
||||
/** 칸 누름 고르기 메뉴(183-3 · 칸 아래 공용 메뉴 · 빈 것 = 안 엶) */
|
||||
choices?: (index: number, key: string) => MapContextMenuItem[] | null;
|
||||
/** 칸 우클릭 메뉴(257-3 · 줄 메뉴가 없는 표 · 빈 것 = 안 엶) */
|
||||
cellMenu?: (index: number, key: string) => MapContextMenuItem[] | null;
|
||||
/** 레벨(201-1 · 공용 표 level · 보기 전용) — 그 열 들여쓰기 */
|
||||
level?: DataTableOptions["level"];
|
||||
/** 같은 값 세로 병합할 열(201-1 · 공용 표 mergeSame) */
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
* 관급 · 별도 줄(183-1 · 서버가 거른 입력) · 수량 = 역전개 총수량 · 비고 = 쓰는 곳(`usesOf`)
|
||||
*
|
||||
* 자재단가대비표 적용 단가 칸 누름 = 고르기 메뉴(183-3 · 값 있는 출처 · 견적 · 「최저(자동)」 →
|
||||
* `pick` · 옛 수기 `applied` 지움) · 고른 출처 단가 칸 강조(`is-picked` · 기본 = 최저 출처) ·
|
||||
* `pick` · 옛 수기 `applied` 지움 · 257-3 우클릭 「적용 단가 바꾸기」 = 같은 목록) ·
|
||||
* 고른 출처 단가 칸 강조(`is-picked` · 기본 = 최저 출처 · 초록 테두리) ·
|
||||
* 관급 / 별도 칸 누름 = 표시 바꿈 · 견적 단가 손입력
|
||||
* · 「견적서」 칸(견적 묶음 아래 · 183-2) 누름 = 파일 모달(`B09_Estimation_UI_Tabs_Base_Quote`)(166-4)
|
||||
* 견적서 글자 = 견적 단가 수기 줄만(185-5 · 나머지 빈칸 · 빈칸 누름 = 안 엶)
|
||||
@@ -177,6 +178,14 @@ export const buildCompare: TabBuild = ({ sheets, user, setUser }): TabTable => {
|
||||
else delete materials[key];
|
||||
setUser({ ...user, materials });
|
||||
};
|
||||
const chooseApplied = (r: Row): MapContextMenuItem[] =>
|
||||
appliedChoices(r, (s) => {
|
||||
const cell = mine(r.key);
|
||||
delete cell.applied; // 옛 수기 적용 단가 — 고르면 풂
|
||||
if (s) cell.pick = s;
|
||||
else delete cell.pick;
|
||||
put(r.key, cell);
|
||||
});
|
||||
return {
|
||||
columns: [
|
||||
qty("no", "번호", 0),
|
||||
@@ -215,18 +224,15 @@ export const buildCompare: TabBuild = ({ sheets, user, setUser }): TabTable => {
|
||||
else cell.quote = v;
|
||||
put(String(row.key), cell);
|
||||
},
|
||||
// 183-3 — 고른 출처(없으면 최저) 단가 칸 강조 · 적용 단가 칸 누름 = 고르기 메뉴
|
||||
// 183-3 — 고른 출처(없으면 최저) 단가 칸 강조(257-3 초록 테두리) · 적용 단가 칸 누름 = 고르기 메뉴
|
||||
cellClass: (i, key) => (key === `p:${rows[i]?.applied_from}` ? "is-picked" : null),
|
||||
choices: (i, key) => {
|
||||
choices: (i, key) => (key === "applied" && rows[i] ? chooseApplied(rows[i]) : null),
|
||||
// 257-3 우클릭 「적용 단가 바꾸기」 → 출처 목록(적용 단가 칸 · 고른 출처 단가 칸 · 최저가여도 바꿈)
|
||||
cellMenu: (i, key) => {
|
||||
const r = rows[i];
|
||||
if (key !== "applied" || !r) return null;
|
||||
return appliedChoices(r, (s) => {
|
||||
const cell = mine(r.key);
|
||||
delete cell.applied; // 옛 수기 적용 단가 — 고르면 풂
|
||||
if (s) cell.pick = s;
|
||||
else delete cell.pick;
|
||||
put(r.key, cell);
|
||||
});
|
||||
if (!r || (key !== "applied" && key !== `p:${r.applied_from}`)) return null;
|
||||
const children = chooseApplied(r).filter((x): x is [string, () => void] => Array.isArray(x));
|
||||
return [{ label: "적용 단가 바꾸기", children }];
|
||||
},
|
||||
onCell: (i, key) => {
|
||||
const row = out[i];
|
||||
|
||||
@@ -101,7 +101,7 @@ def test_배선_수기_적용_칸_출처_돌림_없앰():
|
||||
assert 'editable: (_i, key) => key === "p:견적",' in compare
|
||||
assert "nextPick" not in base and 'key === "applied_from"' not in compare
|
||||
assert 'key === `p:${rows[i]?.applied_from}` ? "is-picked"' in compare
|
||||
assert 'if (key !== "applied" || !r) return null;' in compare
|
||||
assert 'key === "applied" && rows[i] ? chooseApplied(rows[i]) : null' in compare # 257-3 메뉴 둘 공용
|
||||
assert "delete cell.applied" in compare
|
||||
shell = _src("B09_Estimation/B09_Estimation_UI_Shell.ts")
|
||||
assert (
|
||||
@@ -110,4 +110,4 @@ def test_배선_수기_적용_칸_출처_돌림_없앰():
|
||||
edit = _src("B09_Estimation/B09_Estimation_UI_Tabs_Bill_Edit.ts")
|
||||
assert "export function openMenuAt(" in edit and "const ctxMenu = sharedMenu();" in edit
|
||||
css = _src("B09_Estimation/B09_Estimation_UI_Page.css")
|
||||
assert "td.is-picked" in css and "var(--color-accent)" in css
|
||||
assert "td.is-picked" in css and "var(--color-success)" in css # 257-3 초록 테두리
|
||||
|
||||
@@ -65,7 +65,9 @@ def test_좌측_패널_방위_칸_없음_모달_안내():
|
||||
base = modal[
|
||||
modal.index("export function openPlaceModal") : modal.index("interface SiteAddress")
|
||||
]
|
||||
assert "방위는 이 자리로 자동" in base # 244-13 — 기준점 = 등록 모달 분류 기준위치
|
||||
assert (
|
||||
"placePicker(" in base and "방위는 이 자리로 자동" not in base
|
||||
) # 255-3 소재지 머리 줄 지움
|
||||
layout = _read("B08_Quantity/B08_Quantity_Milestone_Layout.ts")
|
||||
assert "placeDeg({ dir: null, lon: b.lon, lat: b.lat }, anchorLL) ??" in layout
|
||||
assert '(anchor === "site" && !used.has(keyOf("site", 270)) ? 270 : free(anchor))' in layout
|
||||
|
||||
@@ -65,7 +65,7 @@ def test_모달_상자_하나_항목_하나_수정_글():
|
||||
leg = _read("B08_Quantity/B08_Quantity_UI_Milestone_LegModal.ts")
|
||||
assert '"+ 줄"' not in modal and "same.rows" not in modal
|
||||
assert 'label: "고침"' not in modal + leg and modal.count('"수정"') >= 2
|
||||
assert 'item.root.style.visibility = dest.root.style.visibility = k === "base"' in modal
|
||||
assert 'item.root.style.display = dest.root.style.display = k === "base"' in modal # 255-3
|
||||
|
||||
|
||||
def test_폭_전체_지도_높이_한_값():
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
"""255-3 거리이정표 거래처 · 기준점 모달 — 칸 이름 ≥ 내용 글 · 「소재지 — 주소 찾기 · 지도에서 고르기」
|
||||
줄 지움 · 기준점 이름 한 줄(빈 칸 자리 비움) · 주소 · 지도 위 가로선 ·
|
||||
256-1 장소 그림 = 등록 · 좌표 바뀜 · 경로 탭 끌기 뒤 capturePlace."""
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
|
||||
|
||||
def _read(rel):
|
||||
return (ROOT / rel).read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def _rule(css, selector):
|
||||
m = re.search(re.escape(selector) + r" \{([^}]*)\}", css)
|
||||
assert m, selector
|
||||
return m.group(1)
|
||||
|
||||
|
||||
def test_모달_꼴():
|
||||
modal = _read("B08_Quantity/B08_Quantity_UI_Milestone_Modal.ts")
|
||||
assert "주소 찾기 · 지도에서 고르기" not in modal and 'el("h4"' not in modal
|
||||
assert (
|
||||
'item.root.style.display = dest.root.style.display = k === "base" ? "none" : "";' in modal
|
||||
)
|
||||
css = _read("B08_Quantity/B08_Quantity_UI_Milestone.css")
|
||||
label = _rule(css, ".b08-milestone__modal .ui-field__label")
|
||||
assert "font-size: var(--text-body-sm);" in label # 입력 글(text-body-sm)과 같음
|
||||
assert "white-space: nowrap;" in label
|
||||
assert "border-top: 1px solid var(--color-border);" in _rule(css, ".b08-milestone__place-field")
|
||||
place = _read("B08_Quantity/B08_Quantity_UI_Milestone_Place.ts")
|
||||
assert 'className: "b08-milestone__place-field"' in place
|
||||
|
||||
|
||||
def test_장소_그림_캡처_부름():
|
||||
modal = _read("B08_Quantity/B08_Quantity_UI_Milestone_Modal.ts")
|
||||
assert modal.count("segment = placed = ") == 2 # 기준점 · 거래처 좌표 바뀜
|
||||
assert "placed = addRoute?.({" in modal and "id: placed," in modal # 새 거래처 · 새 기준점
|
||||
assert "void capturePlace(projectId, placed, {" in modal
|
||||
route = _read("B08_Quantity/B08_Quantity_UI_Milestone_Route.ts")
|
||||
assert "void capturePlace(projectId, key, { name: target.name || label, lon, lat });" in route
|
||||
@@ -0,0 +1,67 @@
|
||||
"""257-3 B09 자재단가대비표 적용 단가 — 초록 테두리 · 우클릭 「적용 단가 바꾸기」(출처 목록).
|
||||
|
||||
- 고른 출처 단가 칸 = 초록 테두리(inset box-shadow · 바탕 색 아님)
|
||||
- 우클릭 = 적용 단가 칸 · 고른 출처 단가 칸 → 「적용 단가 바꾸기」 하위 목록(왼쪽 누름 메뉴와 같은 항목 · 최저가여도 바꿈)
|
||||
- 왼쪽 누름 메뉴(choices)는 그대로
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from resources.tester.helper_b09_group_cards import TABS, run # noqa: E402
|
||||
|
||||
BASE = "B09_Estimation/B09_Estimation_UI_Tabs_Base.ts"
|
||||
PRELUDE = r"""
|
||||
const SOURCES = ["관급", "물가정보", "물가자료", "유통물가", "견적"];
|
||||
const openQuotes = () => {};
|
||||
"""
|
||||
|
||||
JS = """
|
||||
let user = {};
|
||||
const sheets = { compare: [
|
||||
{ key: "m1", no: 1, name: "철근", spec: "D13", unit: "kg", lowest: 1000, lowest_from: "유통물가",
|
||||
applied: 1000, applied_from: "유통물가", pick: null,
|
||||
prices: { 관급: null, 물가정보: 1200, 물가자료: 0, 유통물가: 1000, 견적: null } },
|
||||
] };
|
||||
const t = buildCompare({ sheets, get user() { return user; }, setUser: (u) => (user = u) });
|
||||
const menu = t.cellMenu(0, "applied");
|
||||
const labels = menu[0].children.map((c) => c[0]);
|
||||
menu[0].children[0][1](); // 물가정보 = 최저가 아닌 출처로 바꿈
|
||||
const afterPick = JSON.stringify(user.materials);
|
||||
menu[0].children[1][1](); // 유통물가 = 최저가여도 고른 출처로 박음
|
||||
const afterLowest = JSON.stringify(user.materials);
|
||||
console.log(JSON.stringify({
|
||||
label: menu[0].label, labels, afterPick, afterLowest,
|
||||
onPicked: t.cellMenu(0, "p:유통물가")?.[0].label ?? null,
|
||||
other: [t.cellMenu(0, "name"), t.cellMenu(0, "p:물가정보"), t.cellMenu(5, "applied")],
|
||||
left: t.choices(0, "applied").length, leftOther: t.choices(0, "name"),
|
||||
cls: [t.cellClass(0, "p:유통물가"), t.cellClass(0, "p:물가정보")],
|
||||
}));
|
||||
"""
|
||||
|
||||
|
||||
def test_우클릭_적용_단가_바꾸기_출처_목록():
|
||||
out = run([TABS, BASE], JS, PRELUDE)
|
||||
assert out["label"] == "적용 단가 바꾸기"
|
||||
assert out["labels"] == ["물가정보 1,200", "✓ 유통물가 1,000 · 최저", "✓ 최저(자동)"]
|
||||
assert out["afterPick"] == '{"m1":{"pick":"물가정보"}}'
|
||||
assert out["afterLowest"] == '{"m1":{"pick":"유통물가"}}' # 최저가여도 바꿈
|
||||
assert out["onPicked"] == "적용 단가 바꾸기" # 초록 테두리 칸 우클릭도 같은 메뉴
|
||||
assert out["other"] == [None, None, None]
|
||||
assert (
|
||||
out["left"] == 4 and out["leftOther"] is None
|
||||
) # 왼쪽 누름 메뉴 그대로(출처 둘 · 구분선 · 최저)
|
||||
assert out["cls"] == ["is-picked", None]
|
||||
|
||||
|
||||
def test_배선_쉘_우클릭_CSS_초록_테두리():
|
||||
shell = (ROOT / "B09_Estimation" / "B09_Estimation_UI_Shell.ts").read_text("utf-8")
|
||||
assert 'made.root.addEventListener("contextmenu"' in shell and "spec.cellMenu" in shell
|
||||
assert "openMenuAt(event.clientX, event.clientY, items)" in shell
|
||||
css = (ROOT / "B09_Estimation" / "B09_Estimation_UI_Page.css").read_text("utf-8")
|
||||
box = css.split("td.is-picked {", 1)[1].split("}", 1)[0]
|
||||
assert "box-shadow: inset 0 0 0 2px var(--color-success)" in box and "background" not in box
|
||||
Reference in New Issue
Block a user