Merge remote-tracking branches 'origin/sub_laptop_2' and 'origin/sub_laptop_6' into main_laptop_1

This commit is contained in:
2026-10-08 18:29:38 +09:00
18 changed files with 485 additions and 86 deletions
@@ -18,6 +18,8 @@
import { createButton, el } from "@ui/ui_template_elements";
import { dialogKeys } from "./spreadsheet_actions";
import { parseA1, toA1 } from "./spreadsheet_address";
import { shiftSpan } from "./spreadsheet_refshift";
import { MAX_COLS, MAX_ROWS } from "./spreadsheet_types";
import type { Cell, CellLink, CellRange, Command, Sheet } from "./spreadsheet_types";
import type { SpreadsheetContext } from "./spreadsheet_view_types";
@@ -73,7 +75,7 @@ function shiftLinks(
const along = axis === "col" ? p.r : p.c;
const cross = axis === "col" ? p.c : p.r;
if (along < lo || along > hi || cross < at) return [link]; // 범위 밖 · 밀리는 자리 앞 — 그대로
if (dir < 0 && cross < at + amount) return (changed = true), []; // 지운 자리 — 빠짐
if (dir < 0 && cross < at + amount) return ((changed = true), []); // 지운 자리 — 빠짐
changed = true;
const moved = cross + dir * amount;
return [{ ...link, 칸: axis === "col" ? toA1(p.r, moved) : toA1(moved, p.c) }];
@@ -81,7 +83,42 @@ function shiftLinks(
return changed ? next : undefined;
}
/** 「칸」 명령(값만) 앞에 — 알약이 움직였으면 「시트통째」(연결만 바꿈)를 한 묶음으로(되돌리기 한 걸음) */
/** 밀기 · 당기기에 이름정의 범위도 따라감(114-2 · 보낸 범위 이름 등) — 이름 범위가 `axis` 범위(띠) 안에
* 통째로 들 때만 옮김(띠에 걸치기만 하면 그대로 · 엑셀과 같음) · 다 지운 자리면 빠짐. 안 바뀌면 `undefined`. */
function shiftNames(
sheet: Sheet,
range: CellRange,
axis: "col" | "row",
dir: 1 | -1,
amount: number,
): Sheet["이름정의"] | undefined {
const list = sheet.이름정의;
if (!list?.length) return undefined;
const col = axis === "col";
const [lo, hi] = col ? [range.r0, range.r1] : [range.c0, range.c1];
let changed = false;
const next = list.flatMap((n) => {
const g = n.범위;
if ((col ? g.r0 : g.c0) < lo || (col ? g.r1 : g.c1) > hi) return [n];
const [a, b] = col ? [g.c0, g.c1] : [g.r0, g.r1];
const to = shiftSpan(
a,
b,
dir > 0,
col ? range.c0 : range.r0,
amount,
col ? MAX_COLS : MAX_ROWS,
);
if (to && to[0] === a && to[1] === b) return [n];
changed = true;
if (!to) return [];
return [{ ...n, 범위: col ? { ...g, c0: to[0], c1: to[1] } : { ...g, r0: to[0], r1: to[1] } }];
});
return changed ? next : undefined;
}
/** 「칸」 명령(값만) 앞에 — 알약 · 이름이 움직였으면 「시트통째」(연결 · 이름정의만 바꿈)를 한 묶음으로
* (되돌리기 한 걸음) */
function dispatchShift(
ctx: SpreadsheetContext,
sheet: Sheet,
@@ -93,10 +130,14 @@ function dispatchShift(
): void {
const cellCmd: Command = { 종류: "칸", 시트: ctx.selection.시트, 칸: cells };
const linked = shiftLinks(sheet, range, axis, dir, amount);
const named = shiftNames(sheet, range, axis, dir, amount);
const next = {
...sheet,
...(linked ? { 연결: linked } : {}),
...(named ? { 이름정의: named } : {}),
};
ctx.dispatch(
linked
? { 종류: "묶음", 명령: [{ 종류: "시트통째", 시트: { ...sheet, 연결: linked } }, cellCmd] }
: cellCmd,
linked || named ? { 종류: "묶음", 명령: [{ 종류: "시트통째", 시트: next }, cellCmd] } : cellCmd,
);
}
+10 -2
View File
@@ -182,6 +182,8 @@ export interface PlanKind {
pieces: string[];
/** 양식 — plan(평면도 · 조각 고름) · watershed(유역도 1/6,000 한 장 · 조각 없음). */
layout: PlanKindLayout;
/** 축척 분모 — 평면도 양식은 `scales` 중 하나(기본 1,200) · 유역도는 6,000 고정. */
scale: number;
}
export type PlanKindLayout = "plan" | "watershed";
export interface PlanMark {
@@ -191,6 +193,10 @@ export interface PlanMark {
}
export interface PlanKindsState {
pieces: { id: string; name: string }[];
/** 고를 수 있는 평면도 축척 분모(서버 `DRAWING_SCALE_PLAN_CHOICES`) · 기본 · 유역도 고정. */
scales: number[];
default_scale: number;
watershed_scale: number;
kinds: PlanKind[];
marks: PlanMark[];
/** 종단 측점 간격(m) — 측점 NO + 거리 입력용(GET 만 · 종단이 없으면 null). */
@@ -206,10 +212,11 @@ export function addPlanKind(
name: string,
pieces: string[],
layout: PlanKindLayout = "plan",
scale?: number,
): Promise<PlanKindsState> {
return requestJson(`/projects/${projectId}/plan-kinds`, {
method: "POST",
body: JSON.stringify({ name, pieces, layout }),
body: JSON.stringify({ name, pieces, layout, scale }),
});
}
@@ -218,10 +225,11 @@ export function updatePlanKind(
kindId: string,
name: string,
pieces: string[],
scale?: number,
): Promise<PlanKindsState> {
return requestJson(`/projects/${projectId}/plan-kinds/${encodeURIComponent(kindId)}`, {
method: "PUT",
body: JSON.stringify({ name, pieces }),
body: JSON.stringify({ name, pieces, scale }),
});
}
@@ -153,16 +153,18 @@ def clip_lines(
pieces[piece]["lines"] = kept
def _clip(pieces: dict[str, dict[str, Any]], blocks: list[dict[str, Any]], box: Any) -> None:
def _clip(
pieces: dict[str, dict[str, Any]], blocks: list[dict[str, Any]], box: Any, mm: float = MM
) -> None:
"""표 칸 밑 선 조각(등고선 · 계류 · 노견선 · 외곽선 · 중심선)을 표 밖으로 잘라 낸다."""
min_x, min_y = box[0], box[1]
width = sum(_COLUMNS) / MM
width = sum(_COLUMNS) / mm
rects = [
(
min_x + b["origin"][0] / MM,
min_y + b["origin"][1] / MM - b["rows"] * _ROW_H / MM,
min_x + b["origin"][0] / MM + width,
min_y + b["origin"][1] / MM,
min_x + b["origin"][0] / mm,
min_y + b["origin"][1] / mm - b["rows"] * _ROW_H / mm,
min_x + b["origin"][0] / mm + width,
min_y + b["origin"][1] / mm,
)
for b in blocks
]
@@ -176,6 +178,7 @@ def ip_table_piece(
box: tuple[float, float, float, float],
chunk: dict[str, Any],
total: int,
mm: float = MM,
) -> dict[str, Any]:
"""그 장 IP 표 조각 {blocks} — 자리를 정하고 같은 자리 밑 선 조각을 `pieces` 에서 잘라 낸다."""
blocks = ip_blocks(
@@ -186,9 +189,9 @@ def ip_table_piece(
float(chunk["start_m"]),
float(chunk["end_m"]),
)
_place(blocks, (box[2] - box[0]) * MM)
_place(blocks, (box[2] - box[0]) * mm)
if blocks:
_clip(pieces, blocks, box)
_clip(pieces, blocks, box, mm)
return {"blocks": blocks}
@@ -87,13 +87,15 @@ def plan_area_mm() -> tuple[float, float]:
return (width - (_COMPASS_MARGIN + _COMPASS_SIZE), height - _TITLE_BAND)
def _chunk_span_m() -> tuple[float, float]:
def _chunk_span_m(scale: int = DRAWING_SCALE_PLAN) -> tuple[float, float]:
"""한 장이 담을 수 있는 실거리(m) — 도곽 지형 영역을 축척으로 되돌린 크기."""
area_w, area_h = plan_area_mm()
return (area_w * DRAWING_SCALE_PLAN / 1000.0, area_h * DRAWING_SCALE_PLAN / 1000.0)
return (area_w * scale / 1000.0, area_h * scale / 1000.0)
def plan_chunks(stations: list[tuple[float, float, float]]) -> list[dict[str, Any]]:
def plan_chunks(
stations: list[tuple[float, float, float]], scale: int = DRAWING_SCALE_PLAN
) -> list[dict[str, Any]]:
"""노선을 한 장에 들어가는 구간으로 나눈다. 각 항목: {number, start_m, end_m}.
입력은 종단 측점의 (누가거리 m, x, y)다 — **도면 목록과 도면 생성이 같은 자료**를
@@ -107,7 +109,7 @@ def plan_chunks(stations: list[tuple[float, float, float]]) -> list[dict[str, An
if len(ordered) < 2:
span = (ordered[0][0] if ordered else 0.0, ordered[0][0] if ordered else 0.0)
return [{"number": 1, "start_m": span[0], "end_m": span[1]}]
span_w, span_h = _chunk_span_m()
span_w, span_h = _chunk_span_m(scale)
def fits(part: list[tuple[float, float, float]]) -> bool:
width = max(x for _c, x, _y in part) - min(x for _c, x, _y in part)
@@ -6,7 +6,7 @@
있고, 종류는 그중 무엇을 얹을지 고른 목록이다. 이 모듈은 조각 기하 계산과 도면 조립만
한다(파일 입출력 없음).
좌표 규약: 종이 mm = (사업지 좌표 m - 그 장 도곽 최소점) x MM (1/1,200).
좌표 규약: 종이 mm = (사업지 좌표 m - 그 장 도곽 최소점) x (1000 / 종류 축척 분모) (기본 1/1,200).
"""
import math
@@ -372,7 +372,7 @@ def _reading(nx: float, ny: float) -> tuple[float, float]:
def _piece_entities(
drawing_id: str, piece: str, data: dict[str, Any], paper: Any
drawing_id: str, piece: str, data: dict[str, Any], paper: Any, mm: float = MM
) -> list[dict[str, Any]]:
layer, color = layer_id(piece), _COLORS.get(piece, TABLE_LABEL_COLOR)
out: list[dict[str, Any]] = []
@@ -446,8 +446,8 @@ def _piece_entities(
nx, ny = item["nx"], item["ny"]
line(
[
(item["x"] - nx * _TICK_MM / MM, item["y"] - ny * _TICK_MM / MM),
(item["x"] + nx * _TICK_MM / MM, item["y"] + ny * _TICK_MM / MM),
(item["x"] - nx * _TICK_MM / mm, item["y"] - ny * _TICK_MM / mm),
(item["x"] + nx * _TICK_MM / mm, item["y"] + ny * _TICK_MM / mm),
],
f":{index}",
)
@@ -468,7 +468,7 @@ def _piece_entities(
elif piece == "endpoints":
for index, item in enumerate(data.get("items") or []):
nx, ny = item["nx"], item["ny"]
reach = 6.0 / MM # 종이 6mm 막대
reach = 6.0 / mm # 종이 6mm 막대
line(
[
(item["x"] - nx * reach, item["y"] - ny * reach),
@@ -482,7 +482,7 @@ def _piece_entities(
elif piece == "refuge":
size = 2.0 # 네모 기호 반크기(종이 mm)
for index, item in enumerate(data.get("items") or []):
x, y, half = item["x"], item["y"], size / MM
x, y, half = item["x"], item["y"], size / mm
corners = [(-1, -1), (1, -1), (1, 1), (-1, 1), (-1, -1)]
line([(x + sx * half, y + sy * half) for sx, sy in corners], f":{index}")
cx, cy = paper((x, y))
@@ -531,19 +531,21 @@ def build_plan_kind_drawing(
label: str,
box: tuple[float, float, float, float],
pieces: dict[str, dict[str, Any]],
scale: int = DRAWING_SCALE_PLAN,
) -> dict[str, Any]:
"""고른 조각만 얹어 계획평면도 한 장을 만든다. `box` = 그 장 도곽 범위(실좌표 m)."""
min_x, min_y = box[0], box[1]
mm = 1000.0 / scale
def paper(point: Any) -> tuple[float, float]:
return ((point[0] - min_x) * MM, (point[1] - min_y) * MM)
return ((point[0] - min_x) * mm, (point[1] - min_y) * mm)
entities: list[dict[str, Any]] = []
for piece in PIECE_IDS:
if piece in pieces:
entities.extend(_piece_entities(drawing_id, piece, pieces[piece], paper))
entities.extend(_piece_entities(drawing_id, piece, pieces[piece], paper, mm))
# 도곽 · 방위표 자리는 조각과 무관하게 같은 자리 — 그 장 범위를 기준으로 잡는다.
frame_box = (0.0, 0.0, (box[2] - min_x) * MM, (box[3] - min_y) * MM)
frame_box = (0.0, 0.0, (box[2] - min_x) * mm, (box[3] - min_y) * mm)
entities.extend(
compass_entities(
drawing_id,
@@ -570,7 +572,7 @@ def build_plan_kind_drawing(
entities.append(
_text_entity(
f"{drawing_id}:scale",
f"S = 1/{DRAWING_SCALE_PLAN:,}",
f"S = 1/{scale:,}",
max_bx,
max_by + 5.0,
"b07-plan-title",
@@ -584,7 +586,7 @@ def build_plan_kind_drawing(
drawing_id,
entities_bbox(entities) or bbox,
fit=False,
fields={"도면명": label, **scale_fields(("", DRAWING_SCALE_PLAN))},
fields={"도면명": label, **scale_fields(("", scale))},
)
)
layers = [
@@ -26,6 +26,11 @@ from B07_DesignDetail.B07_DesignDetail_Router_Support_Plan import (
update_kind,
write_marks,
)
from config.config_system import (
DRAWING_SCALE_BASIN,
DRAWING_SCALE_PLAN,
DRAWING_SCALE_PLAN_CHOICES,
)
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/projects", tags=["B07 Design Detail"])
@@ -36,6 +41,8 @@ class PlanKindRequest(BaseModel):
pieces: list[str] = Field(default_factory=list)
# 양식 — plan(평면도 · 조각 고름) · watershed(유역도). 추가에서만 쓰고 업데이트는 그대로 둔다.
layout: str = "plan"
# 축척 분모(`scales` 중 하나) — 없으면 추가는 1/1,200 · 업데이트는 지금 값 · 유역도 1/6,000.
scale: int | None = None
class PlanMark(BaseModel):
@@ -52,6 +59,9 @@ def _state(root: Any) -> dict[str, Any]:
return {
"status": "success",
"pieces": [{"id": piece, "name": name} for piece, name, _locked in PIECES],
"scales": list(DRAWING_SCALE_PLAN_CHOICES),
"default_scale": DRAWING_SCALE_PLAN,
"watershed_scale": DRAWING_SCALE_BASIN,
"kinds": list_kinds(root),
"marks": read_marks(root),
}
@@ -84,7 +94,9 @@ async def post_plan_kind(project_id: UUID, request: PlanKindRequest) -> Any:
"""종류 추가 — 양식 · 조각을 골라 이름을 붙인다."""
try:
root = await _project_root(project_id)
await asyncio.to_thread(add_kind, root, request.name, request.pieces, request.layout)
await asyncio.to_thread(
add_kind, root, request.name, request.pieces, request.layout, request.scale
)
return await asyncio.to_thread(_state, root)
except ValueError as exc:
return _error(400, exc)
@@ -95,7 +107,9 @@ async def put_plan_kind(project_id: UUID, kind_id: str, request: PlanKindRequest
"""종류 업데이트 — 이름 · 조각을 고친다(기본 넷도)."""
try:
root = await _project_root(project_id)
await asyncio.to_thread(update_kind, root, kind_id, request.name, request.pieces)
await asyncio.to_thread(
update_kind, root, kind_id, request.name, request.pieces, request.scale
)
return await asyncio.to_thread(_state, root)
except ValueError as exc:
return _error(400, exc)
@@ -83,6 +83,7 @@ from B07_DesignDetail.B07_DesignDetail_Schema import (
DesignDrawingItem,
)
from common_util.common_util_route_profile import design_elevation_from_longitudinal
from config.config_system import DRAWING_SCALE_PLAN
logger = logging.getLogger(__name__)
@@ -457,7 +458,11 @@ def _read_drawing(
)
else:
drawing = build_plan_kind_drawing(
drawing_id, label, stored_design["box"], stored_design.get("pieces") or {}
drawing_id,
label,
stored_design["box"],
stored_design.get("pieces") or {},
stored_design.get("scale") or DRAWING_SCALE_PLAN,
)
return "plan", label, drawing, False, None
@@ -379,12 +379,12 @@ LIDAR_ID = re.compile(r"^plan_lidar(?:_(\d+))?$")
def _sheet_box(
route_xy: list[tuple[float, float]],
route_xy: list[tuple[float, float]], scale: int = DRAWING_SCALE_PLAN
) -> tuple[float, float, float, float]:
"""그 장의 도곽 범위(실좌표 m) — 계획평면도·용지도·라이다가 같은 규칙을 쓴다."""
area_w_mm, area_h_mm = plan_area_mm()
half_w = area_w_mm / 2.0 * DRAWING_SCALE_PLAN / 1000.0
half_h = area_h_mm / 2.0 * DRAWING_SCALE_PLAN / 1000.0
half_w = area_w_mm / 2.0 * scale / 1000.0
half_h = area_h_mm / 2.0 * scale / 1000.0
center_x = (min(x for x, _y in route_xy) + max(x for x, _y in route_xy)) / 2.0
center_y = (min(y for _x, y in route_xy) + max(y for _x, y in route_xy)) / 2.0
return (
@@ -396,10 +396,10 @@ def _sheet_box(
def _chunk_route(
context: Any, longitudinal: dict[str, Any], number: int
context: Any, longitudinal: dict[str, Any], number: int, scale: int = DRAWING_SCALE_PLAN
) -> tuple[list[tuple[float, float]], dict[str, Any], int]:
"""장 번호로 그 장의 노선 구간을 잘라 낸다 (계획평면도 장 나눔과 같은 기준)."""
chunks = plan_chunks(plan_stations(longitudinal))
chunks = plan_chunks(plan_stations(longitudinal), scale)
chunk = next((item for item in chunks if item["number"] == number), None)
if chunk is None:
raise FileNotFoundError("요청한 장을 찾을 수 없습니다.")
@@ -128,6 +128,7 @@ def place_labels(
parcels: list[dict[str, Any]],
route_xy: list[tuple[float, float]],
box: tuple[float, float, float, float],
mm: float = MM,
) -> list[dict[str, Any]]:
"""필지마다 라벨 [{lines, anchor, center}] — 큰 필지부터 자리를 잡는다."""
clear = LineString(route_xy).buffer(ROUTE_CLEAR_M) if len(route_xy) >= 2 else Polygon()
@@ -142,7 +143,7 @@ def place_labels(
if not any(lines):
continue
width_mm, height_mm = balloon_size(lines, PARCEL_FONT)
half_w, half_h = width_mm / MM / 2.0, height_mm / MM / 2.0
half_w, half_h = width_mm / mm / 2.0, height_mm / mm / 2.0
ax, ay = _anchor(parcel["area"], clear)
free_of_route: tuple[float, float, float, float] | None = None
chosen = None
@@ -189,12 +190,14 @@ def place_labels(
return items
def label_rects(items: list[dict[str, Any]]) -> list[tuple[float, float, float, float]]:
def label_rects(
items: list[dict[str, Any]], mm: float = MM
) -> list[tuple[float, float, float, float]]:
"""필지 정보 상자 자리(사업지 m) — 그리는 상자(`_balloon_entities`)와 같은 크기."""
rects = []
for item in items:
width_mm, height_mm = balloon_size(item["lines"], PARCEL_FONT)
half_w, half_h = width_mm / MM / 2.0, height_mm / MM / 2.0
half_w, half_h = width_mm / mm / 2.0, height_mm / mm / 2.0
cx, cy = item["center"]
rects.append((cx - half_w, cy - half_h, cx + half_w, cy + half_h))
return rects
@@ -205,13 +208,14 @@ def landuse_pieces(
crs: str,
box: tuple[float, float, float, float],
route_xy: list[tuple[float, float]],
mm: float = MM,
) -> dict[str, dict[str, Any]]:
"""그 장의 용지 조각 넷 — 노선은 라벨이 비켜 갈 전체 중심선(사업지 m)."""
folder = project_root / "B04_PreProcess" / "processed"
parcels = clip_parcels(folder / PARCEL_FILE, crs, box)
return {
"parcel": {"lines": [line for parcel in parcels for line in parcel["lines"]]},
"parcel_info": {"items": place_labels(parcels, route_xy, box)},
"parcel_info": {"items": place_labels(parcels, route_xy, box, mm)},
"emd": {"lines": _clip_rings(folder / EMD_FILE, crs, box)},
"sgg": {"lines": _clip_rings(folder / SGG_FILE, crs, box)},
}
@@ -1,7 +1,7 @@
"""B07 계획평면도 종류 · 정보 조각 파일 (PLAN 60장).
저장 자리(프로젝트 저장소 `B07_DesignDetail/`):
plan_kinds/{종류 id}.json 종류 하나 = {name, pieces, order, layout} — 파일 하나씩
plan_kinds/{종류 id}.json 종류 하나 = {name, pieces, order, layout, scale} — 파일 하나씩
plan_pieces/{장}/{조각}.json 정보 조각 하나 = 그 장 범위의 기하(사업지 좌표 m)
plan_marks.json 사용자가 정한 대피소 · 사토장 자리
@@ -65,7 +65,11 @@ from B07_DesignDetail.B07_DesignDetail_Router_Support_Landuse import (
)
from common_util.common_util_json import atomic_write_json
from common_util.common_util_route_profile import design_elevation_from_longitudinal
from config.config_system import DRAWING_SCALE_PLAN
from config.config_system import (
DRAWING_SCALE_BASIN,
DRAWING_SCALE_PLAN,
DRAWING_SCALE_PLAN_CHOICES,
)
KIND_ID = re.compile(r"^[a-z][a-z0-9]{0,31}$")
PLAN_KIND_ID = re.compile(r"^plan_([a-z][a-z0-9]{0,31})(?:_(\d+))?$")
@@ -91,12 +95,21 @@ def _kinds_dir(project_root: Path) -> Path:
_ADDED_ONCE = ("landuse", "watershed")
def _scale(layout: str, value: Any) -> int:
"""종류 축척 분모 — 유역도 양식은 1/6,000 고정 · 없거나 목록 밖이면 1/1,200(옛 파일)."""
if layout == WATERSHED_LAYOUT:
return DRAWING_SCALE_BASIN
return value if value in DRAWING_SCALE_PLAN_CHOICES else DRAWING_SCALE_PLAN
def _document(kind: dict[str, Any], order: int) -> dict[str, Any]:
layout = kind.get("layout", "plan")
return {
"name": kind["name"],
"pieces": list(kind["pieces"]),
"order": order,
"layout": kind.get("layout", "plan"),
"layout": layout,
"scale": _scale(layout, kind.get("scale")),
}
@@ -122,11 +135,17 @@ def _add_defaults_once(folder: Path) -> None:
def list_kinds(project_root: Path) -> list[dict[str, Any]]:
"""종류 목록 [{id, name, pieces, layout}] — 차례대로. 파일이 없으면 기본 종류."""
"""종류 목록 [{id, name, pieces, layout, scale}] — 차례대로. 파일이 없으면 기본 종류."""
folder = _kinds_dir(project_root)
if not folder.is_dir():
return [
{"layout": "plan", **kind, "pieces": list(kind["pieces"])} for kind in DEFAULT_KINDS
{
"layout": "plan",
**kind,
"pieces": list(kind["pieces"]),
"scale": _scale(kind.get("layout", "plan"), None),
}
for kind in DEFAULT_KINDS
]
_add_defaults_once(folder)
rows: list[tuple[int, str, dict[str, Any]]] = []
@@ -151,6 +170,7 @@ def list_kinds(project_root: Path) -> list[dict[str, Any]]:
"name": str(document.get("name") or path.stem),
"pieces": pieces,
"layout": layout,
"scale": _scale(layout, document.get("scale")),
},
)
)
@@ -183,20 +203,44 @@ def _checked(name: str, pieces: list[str], layout: str = "plan") -> tuple[str, l
return text, chosen
def _checked_scale(layout: str, scale: int | None, current: int = DRAWING_SCALE_PLAN) -> int:
"""고른 축척 — 없으면 지금 값 · 목록 밖이면 거절 · 유역도 양식은 1/6,000 고정."""
if layout == WATERSHED_LAYOUT:
return DRAWING_SCALE_BASIN
if scale is None:
return current
if scale not in DRAWING_SCALE_PLAN_CHOICES:
raise ValueError("고를 수 없는 축척입니다.")
return scale
def add_kind(
project_root: Path, name: str, pieces: list[str], layout: str = "plan"
project_root: Path,
name: str,
pieces: list[str],
layout: str = "plan",
scale: int | None = None,
) -> dict[str, Any]:
text, chosen = _checked(name, pieces, layout)
chosen_scale = _checked_scale(layout, scale)
kinds = _materialize(project_root)
if any(kind["name"] == text for kind in kinds):
raise ValueError(f"같은 이름의 종류가 이미 있습니다: {text}")
kind = {"id": f"k{uuid.uuid4().hex[:8]}", "name": text, "pieces": chosen, "layout": layout}
kind = {
"id": f"k{uuid.uuid4().hex[:8]}",
"name": text,
"pieces": chosen,
"layout": layout,
"scale": chosen_scale,
}
atomic_write_json(_kinds_dir(project_root) / f"{kind['id']}.json", _document(kind, len(kinds)))
return kind
def update_kind(project_root: Path, kind_id: str, name: str, pieces: list[str]) -> dict[str, Any]:
"""종류 이름 · 조각을 고친다(차례 · 양식은 그대로). 그 종류의 확정본은 풀어 다시 그린다."""
def update_kind(
project_root: Path, kind_id: str, name: str, pieces: list[str], scale: int | None = None
) -> dict[str, Any]:
"""종류 이름 · 조각 · 축척을 고친다(차례 · 양식은 그대로). 그 종류 확정본은 풀어 다시 그린다."""
if not KIND_ID.fullmatch(kind_id):
raise ValueError("올바르지 않은 종류 id 입니다.")
kinds = _materialize(project_root)
@@ -205,10 +249,17 @@ def update_kind(project_root: Path, kind_id: str, name: str, pieces: list[str])
if current is None or not path.is_file():
raise FileNotFoundError("고칠 종류를 찾을 수 없습니다.")
text, chosen = _checked(name, pieces, current["layout"])
chosen_scale = _checked_scale(current["layout"], scale, current["scale"])
if any(kind["name"] == text and kind["id"] != kind_id for kind in kinds):
raise ValueError(f"같은 이름의 종류가 이미 있습니다: {text}")
order = int(_read_json(path).get("order") or 0)
kind = {"id": kind_id, "name": text, "pieces": chosen, "layout": current["layout"]}
kind = {
"id": kind_id,
"name": text,
"pieces": chosen,
"layout": current["layout"],
"scale": chosen_scale,
}
atomic_write_json(path, _document(kind, order))
manifest = _read_manifest(project_root)
released = False
@@ -383,12 +434,14 @@ def build_pieces(
designs: dict[int | float, dict[str, Any]],
number: int,
wanted: list[str],
scale: int = DRAWING_SCALE_PLAN,
) -> tuple[dict[str, dict[str, Any]], tuple[float, float, float, float], dict[str, Any], int]:
"""그 장의 조각들을 계산해 파일로 쓰고 (조각들, 도곽 범위, 장 구간, 전체 장수)를 낸다."""
route_xy, chunk, total = _chunk_route(context, longitudinal, number)
route_xy, chunk, total = _chunk_route(context, longitudinal, number, scale)
if not route_xy:
raise FileNotFoundError("계획평면도에 그릴 노선이 없습니다.")
box = _sheet_box(route_xy)
box = _sheet_box(route_xy, scale)
mm = 1000.0 / scale
start_m, end_m = float(chunk["start_m"]), float(chunk["end_m"])
project_root = Path(context.project_root)
route = sorted(
@@ -399,9 +452,7 @@ def build_pieces(
pieces: dict[str, dict[str, Any]] = {}
if {"contour", "stream"} & set(wanted):
background = map_background(
project_root, context.crs, DRAWING_SCALE_PLAN, plan_area_mm(), route_xy
)
background = map_background(project_root, context.crs, scale, plan_area_mm(), route_xy)
pieces["contour"] = {"lines": background["contours"]}
pieces["stream"] = {"lines": background["streams"]}
if "centerline" in wanted:
@@ -473,13 +524,13 @@ def build_pieces(
if set(LANDUSE_PIECES) & set(wanted):
# 라벨은 이 장 구간이 아니라 노선 전체를 비켜 간다 — 도곽 안에 다른 구간이 들 수 있다.
pieces.update(
landuse_pieces(project_root, context.crs, box, [(x, y) for _c, x, y in route])
landuse_pieces(project_root, context.crs, box, [(x, y) for _c, x, y in route], mm)
)
if "parcel_info" in wanted:
clip_lines(pieces, label_rects(pieces["parcel_info"]["items"]), LABEL_CLIPPED)
clip_lines(pieces, label_rects(pieces["parcel_info"]["items"], mm), LABEL_CLIPPED)
if "ip_table" in wanted:
pieces["ip_table"] = ip_table_piece(
pieces, _load_design_curves(project_root), route, box, chunk, total
pieces, _load_design_curves(project_root), route, box, chunk, total, mm
)
folder = _design_root(project_root) / "plan_pieces" / str(number)
@@ -510,19 +561,21 @@ def plan_kind_source(
return {"layout": WATERSHED_LAYOUT, "label": kind["name"], **watershed_source(context)}
number = int(match.group(2)) if match.group(2) else 1
pieces, box, chunk, total = build_pieces(
context, longitudinal, longitudinal_path, designs, number, kind["pieces"]
context, longitudinal, longitudinal_path, designs, number, kind["pieces"], kind["scale"]
)
title = plan_title(kind["name"])
return {
"label": title if total <= 1 else f"{title} {chunk['number']}장",
"box": box,
"pieces": pieces,
"scale": kind["scale"],
}
def plan_kind_items(project_root: Path, longitudinal: dict[str, Any]) -> list[dict[str, Any]]:
"""도면 목록에 실을 종류 × 장 [{id, label, plan_kind}]."""
sheets = plan_chunks(plan_stations(longitudinal))
stations = plan_stations(longitudinal)
by_scale: dict[int, list[dict[str, Any]]] = {}
items = []
for kind in list_kinds(project_root):
if kind["layout"] == WATERSHED_LAYOUT: # 1/6,000 한 장 — 장 나눔 없음
@@ -536,6 +589,9 @@ def plan_kind_items(project_root: Path, longitudinal: dict[str, Any]) -> list[di
)
continue
title = plan_title(kind["name"])
if kind["scale"] not in by_scale:
by_scale[kind["scale"]] = plan_chunks(stations, kind["scale"])
sheets = by_scale[kind["scale"]]
for chunk in sheets:
single = len(sheets) <= 1
items.append(
@@ -52,7 +52,7 @@ async function run(action: () => Promise<unknown>, controls: PlanKindsControls):
}
}
/** 양식 — 평면도(조각 고름 · 1/1,200 장 나눔) · 유역도(1/6,000 한 장 · 그림 고정). */
/** 양식 — 평면도(조각 고름 · 축척 고름 · 장 나눔) · 유역도(1/6,000 한 장 · 그림 고정). */
const LAYOUTS: { value: PlanKindLayout; text: string }[] = [
{ value: "plan", text: "평면도" },
{ value: "watershed", text: "유역도" },
@@ -77,6 +77,9 @@ function kindForm(state: PlanKindsState, controls: PlanKindsControls): HTMLEleme
ariaLabel: "양식",
onChange: () => showPieces(),
});
// 축척 — 목록은 서버 `scales` 한 곳 · 유역도 양식은 1/6,000 고정(잠금).
const scaleText = (value: number): string => `1/${value.toLocaleString("en-US")}`;
const scale = createSelectField({ options: [], compact: true, ariaLabel: "축척" });
const name = el("input", "b07-plan-tools__name");
name.type = "text";
name.placeholder = "종류 이름";
@@ -88,7 +91,22 @@ function kindForm(state: PlanKindsState, controls: PlanKindsControls): HTMLEleme
action.type = "button";
// 유역도 양식은 그림이 정해져 있어 얹을 정보를 고르지 않는다.
const showPieces = (): void => {
pieceList.hidden = layout.select.value === "watershed";
const watershed = layout.select.value === "watershed";
pieceList.hidden = watershed;
const kind = state.kinds.find((item) => item.id === pick.select.value);
if (watershed) {
const fixed = String(state.watershed_scale);
scale.setOptions(
[{ value: fixed, text: `${scaleText(state.watershed_scale)} (고정)` }],
fixed,
);
} else {
scale.setOptions(
state.scales.map((value) => ({ value: String(value), text: scaleText(value) })),
String(kind?.layout === "plan" ? kind.scale : state.default_scale),
);
}
scale.select.disabled = watershed;
};
const show = (): void => {
const kind = state.kinds.find((item) => item.id === pick.select.value);
@@ -106,15 +124,16 @@ function kindForm(state: PlanKindsState, controls: PlanKindsControls): HTMLEleme
const kindId = pick.select.value;
const label = name.value.trim();
const chosen = layout.select.value as PlanKindLayout;
const denominator = Number(scale.select.value);
void run(
() =>
kindId
? updatePlanKind(controls.projectId, kindId, label, pieces)
: addPlanKind(controls.projectId, label, pieces, chosen),
? updatePlanKind(controls.projectId, kindId, label, pieces, denominator)
: addPlanKind(controls.projectId, label, pieces, chosen, denominator),
controls,
);
});
form.append(pick.root, layout.root, name, pieceList, action);
form.append(pick.root, layout.root, scale.root, name, pieceList, action);
return form;
}
@@ -401,20 +401,29 @@ export function createAreaToDrawing(opt: AreaToDrawingOptions): AreaToDrawing {
withLinks: (doc) => {
waiting = false;
const { 지운연결: _dropped, ...doc_ } = doc;
doc = doc_;
const old = new Map((doc.연결표 ?? []).map((row) => [row.연결, row]));
const rows: TableLink[] = [];
for (const [sheet, entities] of sheetsOf(doc)) {
for (const e of entities) {
if (!isTable(e)) continue;
const id = linkOf(e);
const row = id ? (old.get(id) ?? fresh.get(id)) : undefined;
if (!id || !row || rows.some((r) => r.연결 === id)) continue;
rows.push({ ...row, 장: sheet, 값: latest.get(id) ?? row.값 });
}
}
// 기록이 없던 도면은 칸을 안 만듦(옛 문서 그대로)
return rows.length || doc.연결표 ? { ...doc, 연결표: rows } : doc;
return fitLinks(doc_, fresh, latest);
},
};
}
/** 도면 저장 문서 `연결표` 를 도면에 남은 표에 맞춤 — 표 없는 줄은 뺌 · 줄 `장` = 표가 놓인 장 ·
* `fresh` = 새로 놓은 표 줄 · `latest` = [업데이트]로 바뀐 값(도면 상세) · 카드 도면 저장도 이 몸(114-2) */
export function fitLinks(
doc: StructureDoc,
fresh: ReadonlyMap<string, TableLink> = new Map(),
latest: ReadonlyMap<string, Record<string, string>> = new Map(),
): StructureDoc {
const old = new Map((doc.연결표 ?? []).map((row) => [row.연결, row]));
const rows: TableLink[] = [];
for (const [sheet, entities] of sheetsOf(doc)) {
for (const e of entities) {
if (!isTable(e)) continue;
const id = linkOf(e);
const row = id ? (old.get(id) ?? fresh.get(id)) : undefined;
if (!id || !row || rows.some((r) => r.연결 === id)) continue;
rows.push({ ...row, 장: sheet, 값: latest.get(id) ?? row.값 });
}
}
// 기록이 없던 도면은 칸을 안 만듦(옛 문서 그대로)
return rows.length || doc.연결표 ? { ...doc, 연결표: rows } : doc;
}
@@ -10,6 +10,7 @@
* ========================================================================== */
import type { StructureDoc } from "./M02_MasterTemplete_Api_Fetch";
import { fitLinks } from "./M02_MasterTemplete_AreaToDrawing";
import type { SaveControl } from "./M02_MasterTemplete_CardShell";
import {
mountStructureSheets,
@@ -130,10 +131,14 @@ export function mountDrawingCard(host: HTMLElement, opt: DrawingCardOptions): Dr
let version = opt.version ?? "";
/** 전체 편집기 [저장]과 같은 길 — 읽은 문서에 `도면` · `더한장`(모든 장)을 갈아 끼워 그 항목 파일에
* (실패는 던짐) · CAD Ctrl+S 가 넘기는 지금 탭 장은 안 씀 — 2장 보다 저장해도 1장 그대로(105-3) */
* (실패는 던짐) · CAD Ctrl+S 가 넘기는 지금 탭 장은 안 씀 — 2장 보다 저장해도 1장 그대로(105-3) ·
* 도면에서 지운 표의 연결표 줄은 뺌(114-2 · 도면 상세 저장과 같은 몸) */
async function doSave(): Promise<void> {
if (!write) return;
const next = { ...doc, ...((await handle.getDoc()) as Pick<StructureDoc, "도면" | "더한장">) };
const next = fitLinks({
...doc,
...((await handle.getDoc()) as Pick<StructureDoc, "도면" | "더한장">),
});
version = (await write("structure", opt.column, version, next)).판;
doc = next;
}
+2
View File
@@ -185,6 +185,8 @@ DRAWING_SCALE_BASIN = 6000 # 유역도 평면 축척 분모 (실거리 1 m = 1/
# 계획평면도·용지도 평면 축척 분모 — 지식DB 「설계제원_총괄」 측량·도면 기준 1/1,200.
# 횡단면도와 같은 원칙: 축척은 줄이지 않고, 한 장에 안 들어가면 **장을 나눈다**.
DRAWING_SCALE_PLAN = 1200
# 평면도 종류마다 고를 수 있는 축척 분모 — 1/1,200 기본(법정) · 1/1,000 · 1/600(용지도).
DRAWING_SCALE_PLAN_CHOICES = (600, 1000, 1200)
# 표준 횡단면도 — 상세도라 지식DB에 지정 축척이 없다. 본 그림 1/50, 측구 부분확대도
# 1/10 (2026-09-04). 노폭 4 m 기준 본 그림이 A1 작도영역에 여유 있게 든다.
DRAWING_SCALE_CROSS_STANDARD = 50
+173
View File
@@ -0,0 +1,173 @@
"""B07 평면도 종류마다 축척 고르기 (PLAN 119장).
① 축척 목록 1/600 · 1/1,000 · 1/1,200(기본) — 추가 · 업데이트 · 다시 읽어 같음 · 목록 밖 거절 ·
옛 파일(축척 없음 · 목록 밖) = 1/1,200 · 유역도 양식 = 1/6,000 고정
② 1/600 → 장 수 늚 · 1/1,200 = 지금과 같음(기본 인자 장 나눔)
③ 그림 — 축척 글 「S = 1/600」 · 도곽 축척 칸 600 · A3 1200 · 1/1,200 은 기본 인자와 같은 그림
④ 종이 mm → 실거리 환산이 축척 따라감 — 측점 눈금 · 시종점 막대 종이 길이 그대로 ·
IP 표 밑 자름 폭 · 필지 라벨 상자 실거리 절반
"""
import json
import math
from pathlib import Path
import pytest
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_IpTable import _COLUMNS, _ROW_H, ip_table_piece
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_Plan import plan_chunks
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_PlanKinds import (
build_plan_kind_drawing,
layer_id,
)
from B07_DesignDetail.B07_DesignDetail_Router_PlanKinds import _state
from B07_DesignDetail.B07_DesignDetail_Router_Support_Landuse import label_rects
from B07_DesignDetail.B07_DesignDetail_Router_Support_Plan import (
_kinds_dir,
add_kind,
list_kinds,
plan_kind_items,
update_kind,
)
_STATIONS = [
{"chainage_m": float(c), "center_x": 0.0, "center_y": float(c)} for c in range(0, 2001, 20)
]
def test_scale_choices_add_update_and_old_files(tmp_path: Path) -> None:
root = tmp_path / "project"
state = _state(root)
assert state["scales"] == [600, 1000, 1200] and state["default_scale"] == 1200
assert all(k["scale"] == (6000 if k["layout"] == "watershed" else 1200) for k in state["kinds"])
added = add_kind(root, "용지 600", ["contour"], "plan", 600)
assert added["scale"] == 600
assert next(k for k in list_kinds(root) if k["id"] == added["id"]) == added
assert add_kind(root, "기본 축척", ["contour"])["scale"] == 1200
update_kind(root, added["id"], "용지 1000", ["contour"], 1000)
assert next(k for k in list_kinds(root) if k["id"] == added["id"])["scale"] == 1000
update_kind(root, added["id"], "용지 1000", ["contour"]) # 축척 없이 = 지금 값
assert next(k for k in list_kinds(root) if k["id"] == added["id"])["scale"] == 1000
with pytest.raises(ValueError):
add_kind(root, "이상한 축척", ["contour"], "plan", 500)
with pytest.raises(ValueError):
update_kind(root, added["id"], "용지 1000", ["contour"], 777)
watershed = add_kind(root, "유역 둘", [], "watershed", 600) # 유역도 양식은 고정
assert watershed["scale"] == 6000
folder = _kinds_dir(root)
for stem, extra in (("old", {}), ("odd", {"scale": 777})):
(folder / f"{stem}.json").write_text(
json.dumps({"name": stem, "pieces": ["contour"], "order": 99, **extra}), "utf-8"
)
kinds = {k["id"]: k for k in list_kinds(root)}
assert kinds["old"]["scale"] == 1200 and kinds["odd"]["scale"] == 1200
def test_sheet_count_follows_scale(tmp_path: Path) -> None:
root = tmp_path / "project"
add_kind(root, "용지 600", ["contour"], "plan", 600)
items = plan_kind_items(root, {"stations": _STATIONS})
big = [item for item in items if item["plan_kind_name"] == "용지 600"]
terrain = [item for item in items if item["plan_kind"] == "terrain"]
stations = [(s["chainage_m"], s["center_x"], s["center_y"]) for s in _STATIONS]
assert len(terrain) == len(plan_chunks(stations)) == len(plan_chunks(stations, 1200))
assert len(big) == len(plan_chunks(stations, 600)) > len(terrain)
assert big[-1]["label"].endswith(f"{len(big)}장")
def _texts(drawing: dict) -> str:
return json.dumps(drawing["entities"], ensure_ascii=False)
def _frame_labels(drawing: dict) -> set[str]:
return {
(e.get("shapeData") or {}).get("label")
for e in drawing["entities"]
if e.get("layerId") == "b08-frame" and e["type"] == "Text"
}
def _segment(entity: dict) -> tuple[float, float, float]:
"""두 점 PolyLine(자식 Line 하나) — (길이, 가운데 x, 가운데 y) 종이 mm."""
(child,) = entity["children"]
a, b = child["shapeData"]["startPoint"], child["shapeData"]["endPoint"]
return (
math.hypot(b["x"] - a["x"], b["y"] - a["y"]),
(a["x"] + b["x"]) / 2.0,
(a["y"] + b["y"]) / 2.0,
)
def test_drawing_scale_text_frame_and_paper_lengths() -> None:
box = (0.0, 0.0, 400.0, 300.0)
pieces = {
"station": {
"items": [
{"x": 100.0, "y": 100.0, "nx": 1.0, "ny": 0.0, "label": "NO.5", "major": True}
]
},
"endpoints": {"items": [{"label": "시점", "x": 50.0, "y": 50.0, "nx": 0.0, "ny": 1.0}]},
}
base = build_plan_kind_drawing("plan_x", "시험", box, pieces)
assert build_plan_kind_drawing("plan_x", "시험", box, pieces, 1200) == base
small = build_plan_kind_drawing("plan_x", "시험", box, pieces, 600)
assert "S = 1/1,200" in _texts(base) and "S = 1/600" in _texts(small)
assert "S = 1/1,200" not in _texts(small)
scales = [
e
for e in small["entities"]
if str((e.get("shapeData") or {}).get("label")).startswith("S = ")
]
assert [e["shapeData"]["label"] for e in scales] == ["S = 1/600"]
# 도곽 축척 칸 — A1 분모 600 · A3 1200 (1/1,200 그림은 1200 · 2400)
assert {"600", "1200"} <= _frame_labels(small) and "2400" not in _frame_labels(small)
assert {"1200", "2400"} <= _frame_labels(base) and "600" not in _frame_labels(base)
# 종이 mm 기호는 축척과 무관하게 같은 길이 · 실거리 자리는 축척만큼 멀어짐
for piece, paper_mm in (("station", 3.0), ("endpoints", 12.0)):
lines = [
[
e
for e in d["entities"]
if e.get("layerId") == layer_id(piece) and e["type"] != "Text"
]
for d in (base, small)
]
(length, x, y), (length_s, x_s, y_s) = _segment(lines[0][0]), _segment(lines[1][0])
assert length == pytest.approx(paper_mm) and length_s == pytest.approx(paper_mm)
assert (x_s, y_s) == (pytest.approx(2 * x), pytest.approx(2 * y)) # 실거리 자리는 2배
# test_b07_plan_kinds 의 IP 표 시험과 같은 노선 · 곡선
_ROUTE = [(0.0, 0.0, 0.0), (85.0, 85.0, 0.0), (115.0, 100.0, 15.0), (200.0, 100.0, 100.0)]
_CURVES = [
{"apex": [500.0, 500.0], "start": [490.0, 500.0], "end": [500.0, 510.0], "radius_m": 10.0},
{"apex": [100.0, 0.0], "start": [85.0, 0.0], "end": [100.0, 15.0], "radius_m": 15.0},
]
def _gap(mm: float) -> tuple[float, int]:
"""y = 0 등고선에서 잘려 나간 길이(m) · 그 줄을 덮는 표 수."""
box = (-20.0, -20.0, 300.0, 200.0)
pieces = {"contour": {"lines": [[[-20.0, 0.0], [300.0, 0.0]]]}}
table = ip_table_piece(
pieces, _CURVES, _ROUTE, box, {"number": 1, "start_m": 0.0, "end_m": 200.0}, 1, mm
)
kept = sum(line[-1][0] - line[0][0] for line in pieces["contour"]["lines"])
covering = sum(
1
for b in table["blocks"]
if box[1] + (b["origin"][1] - b["rows"] * _ROW_H) / mm < 0.0 < box[1] + b["origin"][1] / mm
)
return 320.0 - kept, covering
def test_ip_table_clip_and_parcel_label_follow_scale() -> None:
assert _gap(1000.0 / 1200) == (pytest.approx(43.2), 1) # 지금 그림(test_b07_plan_kinds)과 같음
# 표 종이 폭 36 mm 그대로 → 1/600 실거리 21.6 m 씩(덮는 표마다)
removed, covering = _gap(1000.0 / 600)
assert covering >= 1 and removed == pytest.approx(sum(_COLUMNS) * 0.6 * covering)
items = [{"lines": ["1-1", "임야"], "center": [0.0, 0.0]}]
(big,) = label_rects(items)
(half,) = label_rects(items, 1000.0 / 600)
assert half[2] - half[0] == pytest.approx((big[2] - big[0]) / 2)
assert half[3] - half[1] == pytest.approx((big[3] - big[1]) / 2)
+2
View File
@@ -247,6 +247,7 @@ def test_update_kind_default_and_release_confirmed(tmp_path: Path) -> None:
"name": "지형2",
"pieces": ["contour", "centerline"],
"layout": "plan",
"scale": 1200,
}
assert [k["id"] for k in list_kinds(root)][0] == "terrain" # 차례 그대로
drawings = _read_manifest(root)["drawings"]
@@ -375,6 +376,7 @@ def test_watershed_kind_layout_one_sheet(tmp_path: Path) -> None:
"name": "배수유역도",
"pieces": [],
"layout": "watershed",
"scale": 6000,
}
# 측점 0 ~ 2,000 m — 1/1,200 평면도는 여러 장 · 유역도는 한 장
stations = [
+55 -1
View File
@@ -2,6 +2,7 @@
- 114-1 면적 상세를 열 때 화면(`sentRanges` + 책)이 `도면_{연결}` 이름정의를 둠 → 행 넣기 → 면적 [저장]
→ 서버 `refresh_drawing` 이 그 이름을 연결표 줄에 적고 도면 표 칸 글 = 옮겨진 칸.
- 114-2 셀 밀기 · 당기기(칸 삽입 · 삭제 창)에 이름 따라감 · 카드 도면 저장도 표 없는 연결표 줄 뺌(`fitLinks`).
TS 는 Node 로(`AreaToDrawing` 은 그 자리 변환 · 스프레드시트 모듈은 `helper_spreadsheet_d_bundle.cjs`).
"""
@@ -66,7 +67,36 @@ const { applyCommand } = ss("spreadsheet_commands");
names: book.시트[0].이름정의,
};
applyCommand(book, { 종류: "행넣기", 시트: "s1", at: 0, 수: 1 }); // 1행 위에 한 행
console.log(JSON.stringify({ opened, book }));
// 114-2 — 셀 밀기 · 당기기(칸 삽입 · 삭제 창)에 이름 따라감 · 되돌리기 한 걸음
const dlg = ss("spreadsheet_insert_dialog");
const E7J17 = { r0: 6, c0: 4, r1: 16, c1: 9 };
const shift = (fn, range) => {
const b = { 종류: "통합문서", 판: 1, 열: "t", 서식: [{}], 시트: [
{ id: "s1", 이름: "s1", 칸: { E7: { 값: 1 } }, 이름정의: [{ 이름: "도면_T1", 범위: E7J17 }] },
] };
const sent = [];
const ctx = { book: b, readOnly: false, selection: { 시트: "s1" }, sheet: () => b.시트[0],
dispatch: (c) => (sent.push(c.종류 === "묶음" ? c.명령.map((x) => x.종류).join("+") : c.종류), applyCommand(b, c)) };
dlg[fn](ctx, range);
const n = b.시트[0].이름정의.map((x) => x.범위);
return { names: n, sent, cell: Object.keys(b.시트[0].칸) };
};
const shifted = {
downInside: shift("insertShiftDown", { r0: 9, c0: 4, r1: 9, c1: 9 }),
downAbove: shift("insertShiftDown", { r0: 4, c0: 4, r1: 4, c1: 9 }),
downPartial: shift("insertShiftDown", { r0: 4, c0: 4, r1: 4, c1: 5 }),
upAll: shift("deleteShiftUp", E7J17),
right: shift("insertShiftRight", { r0: 6, c0: 3, r1: 16, c1: 3 }),
left: shift("deleteShiftLeft", { r0: 6, c0: 0, r1: 16, c1: 1 }),
};
// 114-2 — 카드 도면 저장도 표 없는 연결표 줄을 뺌(도면 상세 저장과 같은 몸)
const tbl = (id) => ({ type: "Table", shapeData: { cells: [[{ text: "x" }]], style: { 연결: id } } });
const row = (id) => ({ 연결: id, 시트: "s1", 범위: "A1", 값: {} });
const fit = {
kept: toDrawing.fitLinks({ 도면: { entities: [tbl("T1")] }, 연결표: [row("T1"), row("T2")] }).연결표,
plain: "연결표" in toDrawing.fitLinks({ 도면: { entities: [tbl("T9")] } }),
};
console.log(JSON.stringify({ opened, book, shifted, fit }));
})().catch((e) => {
console.error(e);
process.exit(1);
@@ -134,3 +164,27 @@ def test_옛_연결_열기_행_넣기_저장_도면_표_값_따라감_114_1(worl
cells = doc["도면"]["entities"][0]["shapeData"]["cells"]
assert _texts(cells) == _texts(want["표"]["shapeData"]["cells"]) # 옮겨진 칸 = 보낸 그 칸
assert _texts(cells)[0] == ["제목", "1,234.50"]
def test_셀_밀기_당기기에_이름_따라감_띠에_걸치기만_하면_그대로_114_2(world): # noqa: F811
with store.at(world["system"]):
area = store.read("area", "rv")
s = _node({"book": area["문서"], "drawing": {"연결표": []}})["shifted"]
box = lambda r0, c0, r1, c1: [{"r0": r0, "c0": c0, "r1": r1, "c1": c1}] # noqa: E731
assert s["downInside"]["names"] == box(6, 4, 17, 9) # 범위 안에 밀어 넣음 = 늘어남 E7:J18
assert s["downAbove"]["names"] == box(7, 4, 17, 9) # 위에서 밀림 = 통째로 E8:J18
assert s["downPartial"]["names"] == box(6, 4, 16, 9) # 띠(E:F)에 걸치기만 = 그대로
assert s["upAll"]["names"] == [] # 다 지운 자리 = 이름 빠짐(저장 때 연결 끊김)
assert s["right"]["names"] == box(6, 5, 16, 10) # F7:K17
assert s["left"]["names"] == box(6, 2, 16, 7) # C7:H17
assert s["downInside"]["sent"] == ["시트통째+칸"] # 한 묶음 = 되돌리기 한 걸음
assert s["downPartial"]["sent"] == ["칸"] # 안 바뀌면 시트통째 안 끼움
assert s["downAbove"]["cell"] == ["E8"] # 값도 함께 밀림
def test_카드_도면_저장도_표_없는_연결표_줄_뺌_114_2(world): # noqa: F811
with store.at(world["system"]):
area = store.read("area", "rv")
fit = _node({"book": area["문서"], "drawing": {"연결표": []}})["fit"]
assert fit["kept"] == [{"연결": "T1", "시트": "s1", "범위": "A1", "값": {}, "장": ""}]
assert fit["plain"] is False # 기록 없던 도면은 칸을 안 만듦
+2 -2
View File
@@ -40,9 +40,9 @@ def test_메뉴는_숨기고_저장은_문서_전체():
assert part in card.split("const HIDE_EDIT")[1].split(";\n")[0]
# 도번 · 열 · 판 은 읽은 그대로 · 모든 장(지금 탭 하나가 1장을 덮던 흠 · 105-3)
assert (
'const next = { ...doc, ...((await handle.getDoc()) as Pick<StructureDoc, "도면" | "더한장">) };'
'const next = fitLinks({\n ...doc,\n ...((await handle.getDoc()) as Pick<StructureDoc, "도면" | "더한장">),\n });'
in card
)
) # 지운 표의 연결표 줄은 뺌(114-2 · 도면 상세 저장과 같은 몸)
assert "async function doSave(): Promise<void>" in card
assert 'write("structure", opt.column, version, next)' in card
for key in ("L · PL", "E · Delete", "Ctrl+Z · Ctrl+Y", "Ctrl+S"):