feat(638-2): 예정노선 노선 세트 화면 셈 — 세트 지정 · 끝에서 나눔 · 이웃 합침 · 고를 세트 · [잇기](요구 547 · 548)
- 새 logic/D03_PlanAnalysis_Group.ts: assignSet(고른 구간 끝 나눔 → 세트 → 같은 선 이웃 같은 세트 하나 · 앞 id) · mergeSets(서버 Network.derive 짝) · setChoices(있는 세트 + 해마다 새 번호) · joinEnds(닿음 바로 · 떨어짐 곧은 토막 덧 = 앞 선 세트) - Plan.ts 세 곳: PlanPiece.set · 새 선 처음 세트(사업연도 시작 해 새 번호 · 갈래 = 붙은 자리 세트 · 옛 꼴 = 그 해 최댓값 + 1) · 이음 합침 비교 = 세트 · 661 줄 그대로 - Select.ts 끌어 고르기 시험 조각에 set 칸(타입 맞춤) - Api.ts 꼴: derived.sets(PlanSetView · 638-1 서버 셈) - 시험 test_638_group.py · 거울 사례 fixtures/d03_638_set_cases.json(sub3 638-1 과 같은 파일) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
* D03 분석 화면 서버 요청(PLAN 563-1) — 지형 받기 범위 · 받기 · 진행 ·
|
||||
* 사업 전체 지형면 한 벌(`models_plan/` · 격자) · 그은 선 정본(562-2 GET · PUT network · 지점 · 기준점 함께 591-2 ·
|
||||
* 지점 · 선 이름 · 노드 기록 · [저장] 기준점 594) · 주소 · 장소 찾기(594-4 공용 `/map/search`) ·
|
||||
* 라이다 지형(624-4 · LAS 올리기 · 지은 지형 · 높이 격자) ·
|
||||
* 라이다 지형(624-4 · LAS 올리기 · 지은 지형 · 높이 격자) · 노선 세트 꼴(638 · 조각 `set` · `derived.sets`) ·
|
||||
* 지도 겹 받기 = 공용 `A00_Common/map/map_layer_api`(624-3 · 다시 내놓음).
|
||||
* ========================================================================== */
|
||||
|
||||
@@ -82,12 +82,25 @@ export interface ServerNetwork {
|
||||
pieces: PlanPiece[];
|
||||
}
|
||||
|
||||
/** 서버가 [저장] · 읽기 때 센 세트(638-1) — 끊김 · 갈래 경고는 이 값으로만(화면 셈 없음). */
|
||||
export interface PlanSetView {
|
||||
key: string;
|
||||
year: number;
|
||||
no: number;
|
||||
parts: number;
|
||||
branch: boolean;
|
||||
length_m: number;
|
||||
pieces: string[];
|
||||
}
|
||||
|
||||
export interface PlanNetworkView {
|
||||
base: PlanBaseView;
|
||||
network: ServerNetwork | null;
|
||||
derived: {
|
||||
lines: Array<{ id: string; length_m: number; stations: number[] }>;
|
||||
pieces: Array<PlanPiece & { length_m: number; planned: Vertex[] }>;
|
||||
/** 세트(638) — 열쇠 「연도-번호」 · 덩이 수(`parts` > 1 = 끊김) · 세트 안 갈래 · 조각 id. */
|
||||
sets: PlanSetView[];
|
||||
} | null;
|
||||
/** 사업연도 시작(있으면). */
|
||||
year_start?: number | null;
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
/* =============================================================================
|
||||
* D03_PlanAnalysis_Group.ts
|
||||
* D 예정노선 노선 세트(PLAN 638-2 · 요구 547 · 548) — 세트 = 「연도-번호」(2027-1) · 같은 번호 = 갈라져 있어도 같은 노선.
|
||||
* 세트 지정(고른 구간 끝에서 조각 나눔 → 세트 바꿈 → 같은 선 이웃 같은 세트 하나로) · 고를 세트 목록 ·
|
||||
* [잇기](두 선 끝 · 닿으면 바로 · 떨어지면 곧은 토막 덧 · 이은 토막 = 앞 선 세트).
|
||||
* 새 선 처음 세트 · 갈래 · 이어 긋기 = `D03_PlanAnalysis_Plan.normalizePieces`.
|
||||
* ⚠ 짝 = `D03_PlanAnalysis_Network.derive` 세트 합침(서버 [저장] 정본) — 거울 시험 638(`fixtures/d03_638_set_cases.json`).
|
||||
* 끊김 · 갈래 경고 셈은 서버 한 곳(여기 없음). DOM · fetch · sessionStorage 없음.
|
||||
* ========================================================================== */
|
||||
|
||||
import {
|
||||
appendVertex,
|
||||
attachHead,
|
||||
nextSet,
|
||||
normalizePieces,
|
||||
reverseLine,
|
||||
samePoint,
|
||||
splitPiece,
|
||||
type PlanNetwork,
|
||||
type PlanPiece,
|
||||
} from "./D03_PlanAnalysis_Plan";
|
||||
|
||||
/** 세트 — 연도 · 그 해 번호(1 부터). */
|
||||
export interface PieceSet {
|
||||
year: number;
|
||||
set: number;
|
||||
}
|
||||
|
||||
/** 이웃 조각이 맞닿았다고 볼 틈(m) — 서버 합침과 같음(1 cm). */
|
||||
const TOUCH_M = 0.01;
|
||||
|
||||
/** 세트 열쇠 「연도-번호」(업체 지정 · B 받기 단위). */
|
||||
export const setKey = (s: PieceSet): string => `${s.year}-${s.set}`;
|
||||
|
||||
export const sameSet = (a: PieceSet, b: PieceSet): boolean => a.year === b.year && a.set === b.set;
|
||||
|
||||
/** 같은 선 이웃(from_m 차례 · 맞닿음) 같은 세트 = 하나(앞 조각 id · 사이 마크 걷음) · 다른 선끼리 안 합침 ·
|
||||
* `lines` = 이 선만(없으면 모두) · 결과 = 선 차례 · from_m 차례. */
|
||||
export function mergeSets(network: PlanNetwork, lines?: ReadonlySet<string>): PlanNetwork {
|
||||
const pieces: PlanPiece[] = [];
|
||||
for (const line of network.lines) {
|
||||
const own = network.pieces
|
||||
.filter((piece) => piece.line === line.id)
|
||||
.sort((a, b) => a.from_m - b.from_m);
|
||||
for (const piece of own) {
|
||||
const last = pieces[pieces.length - 1];
|
||||
const join =
|
||||
(!lines || lines.has(line.id)) &&
|
||||
last?.line === line.id &&
|
||||
sameSet(last, piece) &&
|
||||
Math.abs(last.to_m - piece.from_m) <= TOUCH_M;
|
||||
if (join) pieces[pieces.length - 1] = { ...last, to_m: piece.to_m };
|
||||
else pieces.push({ ...piece });
|
||||
}
|
||||
}
|
||||
return { ...network, pieces };
|
||||
}
|
||||
|
||||
/** 선 위 구간(m). */
|
||||
export interface LineRange {
|
||||
line: string;
|
||||
from_m: number;
|
||||
to_m: number;
|
||||
}
|
||||
|
||||
/** 세트 지정 — 고른 구간 끝에서 조각을 나눔(끝 · 경계와 1 m 안이면 그 경계) → 가운데가 구간 안인 조각 = `target` →
|
||||
* 고친 선 이웃 같은 세트 하나로. 연도를 바꾸면 `target` = 그 해 새 세트(`setChoices`). */
|
||||
export function assignSet(
|
||||
network: PlanNetwork,
|
||||
ranges: ReadonlyArray<LineRange>,
|
||||
target: PieceSet,
|
||||
): PlanNetwork {
|
||||
let next = network;
|
||||
for (const range of ranges)
|
||||
for (const at of [range.from_m, range.to_m]) next = splitPiece(next, range.line, at);
|
||||
const inside = (piece: PlanPiece): boolean =>
|
||||
ranges.some((range) => {
|
||||
const mid = (piece.from_m + piece.to_m) / 2;
|
||||
const [a, b] = [Math.min(range.from_m, range.to_m), Math.max(range.from_m, range.to_m)];
|
||||
return range.line === piece.line && mid > a && mid < b;
|
||||
});
|
||||
const pieces = next.pieces.map((piece) =>
|
||||
inside(piece) ? { ...piece, year: target.year, set: target.set } : piece,
|
||||
);
|
||||
return mergeSets({ ...next, pieces }, new Set(ranges.map((range) => range.line)));
|
||||
}
|
||||
|
||||
/** 고를 세트 — 있는 세트 + 해마다 「새 세트」(`fresh` · 번호 = 그 해 최댓값 + 1) · 연도 · 번호 차례. */
|
||||
export interface SetChoice extends PieceSet {
|
||||
key: string;
|
||||
fresh: boolean;
|
||||
}
|
||||
|
||||
export function setChoices(network: PlanNetwork, years: ReadonlyArray<number>): SetChoice[] {
|
||||
const have = new Map<string, PieceSet>();
|
||||
for (const piece of network.pieces)
|
||||
if (piece.set > 0) have.set(setKey(piece), { year: piece.year, set: piece.set });
|
||||
const allYears = new Set([...years, ...[...have.values()].map((s) => s.year)]);
|
||||
const out: SetChoice[] = [];
|
||||
for (const year of [...allYears].sort((a, b) => a - b)) {
|
||||
const own = [...have.values()].filter((s) => s.year === year).sort((a, b) => a.set - b.set);
|
||||
for (const s of own) out.push({ ...s, key: setKey(s), fresh: false });
|
||||
const fresh = { year, set: nextSet(network.pieces, year) };
|
||||
out.push({ ...fresh, key: setKey(fresh), fresh: true });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 선 끝 — 선 id · 머리(첫 점) 쪽인가. */
|
||||
export interface LineEnd {
|
||||
line: string;
|
||||
head: boolean;
|
||||
}
|
||||
|
||||
const indexOf = (network: PlanNetwork, id: string): number =>
|
||||
network.lines.findIndex((line) => line.id === id);
|
||||
|
||||
const endPoint = (network: PlanNetwork, end: LineEnd): [number, number] => {
|
||||
const v = network.lines[indexOf(network, end.line)].vertices;
|
||||
return end.head ? v[0] : v[v.length - 1];
|
||||
};
|
||||
|
||||
/** `id` 선 방향 바꿈(갈래 = null). */
|
||||
function flip(network: PlanNetwork, id: string): PlanNetwork | null {
|
||||
const out = reverseLine(network, indexOf(network, id));
|
||||
return typeof out === "string" ? null : out;
|
||||
}
|
||||
|
||||
/** `x` 끝이 꼬리 · `y` 끝이 머리가 되게 돌려 `y` 를 `x` 꼬리에 이음 — 못 하면 null. */
|
||||
function joinAt(network: PlanNetwork, x: LineEnd, y: LineEnd): PlanNetwork | null {
|
||||
let next: PlanNetwork | null = network;
|
||||
if (x.head) next = flip(next, x.line);
|
||||
if (next && !y.head) next = flip(next, y.line);
|
||||
if (!next) return null;
|
||||
const xn = next.lines[indexOf(next, x.line)].vertices.length;
|
||||
const yn = next.lines[indexOf(next, y.line)].vertices.length;
|
||||
const joined = attachHead(next, indexOf(next, y.line));
|
||||
const after = joined.lines[indexOf(joined, x.line)];
|
||||
// 다른 선 꼬리에 붙거나 갈래가 됨 = 이 두 선 이음이 아님
|
||||
return indexOf(joined, y.line) < 0 && after?.vertices.length === xn + yn - 1 ? joined : null;
|
||||
}
|
||||
|
||||
/** [잇기] — 두 선 끝을 한 선으로 · 떨어져 있으면 `a` 끝에서 `b` 끝까지 곧은 토막을 덧댐(그 토막 = `a` 끝 조각 세트) ·
|
||||
* 이음 자리 두 조각이 같은 세트면 하나 · 못 이음 = 까닭 글. `year` = 새 선 처음 해(`normalizePieces`). */
|
||||
export function joinEnds(
|
||||
network: PlanNetwork,
|
||||
a: LineEnd,
|
||||
b: LineEnd,
|
||||
year: number,
|
||||
): PlanNetwork | string {
|
||||
const [ia, ib] = [indexOf(network, a.line), indexOf(network, b.line)];
|
||||
if (ia < 0 || ib < 0) return "이을 선이 없습니다.";
|
||||
if (ia === ib) return "같은 선의 두 끝은 이을 수 없습니다.";
|
||||
const isBranch = (end: LineEnd): boolean =>
|
||||
end.head && !!network.lines[indexOf(network, end.line)].branch;
|
||||
if (isBranch(a) || isBranch(b)) return "갈래 첫 점은 붙은 선 위라 이을 수 없습니다.";
|
||||
let next = network;
|
||||
const to = endPoint(network, b);
|
||||
if (!samePoint(endPoint(network, a), to)) {
|
||||
// `a` 끝에 곧은 토막 — 머리 쪽이면 뒤집어 꼬리에 덧대고 다시 뒤집음(조각이 따라감)
|
||||
if (a.head) next = flip(next, a.line) ?? next;
|
||||
next = normalizePieces(appendVertex(next, indexOf(next, a.line), [to[0], to[1]]), year);
|
||||
if (a.head) next = flip(next, a.line) ?? next;
|
||||
}
|
||||
// 덜 뒤집는 쪽 먼저(머리 · 꼬리면 뒤 선 꼬리에 앞 선)
|
||||
const [x, y] = a.head && !b.head ? [b, a] : [a, b];
|
||||
const joined = joinAt(next, x, y) ?? joinAt(next, y, x);
|
||||
return joined ? normalizePieces(joined, year) : "두 선을 이을 수 없습니다 — 선 끝을 고르세요.";
|
||||
}
|
||||
@@ -55,13 +55,14 @@ export function nameError(name: string): string | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** 조각 = 선(id) 위 [from_m, to_m] 구역 · 연도(구역 마크 = 조각 경계 · id = 업체 지정이 붙는 열쇠라 나눠도 앞 조각이 지님). */
|
||||
/** 조각 = 선(id) 위 [from_m, to_m] 구역 · 연도 · 세트 번호(638 열쇠 「연도-번호」 · 셈 `D03_PlanAnalysis_Group`) — 마크 = 조각 경계 · id 는 나눠도 앞 조각이 지님. */
|
||||
export interface PlanPiece {
|
||||
id: string;
|
||||
line: string;
|
||||
from_m: number;
|
||||
to_m: number;
|
||||
year: number;
|
||||
set: number;
|
||||
}
|
||||
|
||||
export interface PlanNetwork {
|
||||
@@ -76,6 +77,9 @@ const SAME_M = 1e-3;
|
||||
|
||||
const round3 = (value: number): number => Math.round(value * 1000) / 1000;
|
||||
export const samePoint = (a: Vertex, b: Vertex): boolean => segmentLength(a, b) <= SAME_M;
|
||||
/** 그 해 새 세트 번호 = 있는 번호 최댓값 + 1(638 · 지워도 다른 번호 안 당김). */
|
||||
export const nextSet = (pieces: ReadonlyArray<PlanPiece>, year: number): number =>
|
||||
Math.max(0, ...pieces.filter((p) => p.year === year && p.set > 0).map((p) => p.set)) + 1;
|
||||
|
||||
/** 새 조각 id — `p<n>`(있는 것 중 가장 큰 수 + 1). */
|
||||
function nextPieceId(pieces: ReadonlyArray<PlanPiece>): string {
|
||||
@@ -107,8 +111,8 @@ export function newLine(network: PlanNetwork, point: Vertex): PlanNetwork {
|
||||
return { ...network, lines: [...network.lines, line] };
|
||||
}
|
||||
|
||||
/** 새 갈래 처음 해(585-3) = 붙은 자리 구간 해 · 붙은 자리 못 찾음 = null. */
|
||||
function branchYear(network: PlanNetwork, line: PlanLine): number | null {
|
||||
/** 새 갈래 처음 해 · 세트(585-3 · 638) = 붙은 자리 조각 · 못 찾음 = null. */
|
||||
function branchPiece(network: PlanNetwork, line: PlanLine): PlanPiece | null {
|
||||
const parent = network.lines.find((other) => other.id === line.branch?.line);
|
||||
if (!parent || !line.branch) return null;
|
||||
const atM = lineLength(parent.vertices.slice(0, line.branch.vertex + 1));
|
||||
@@ -116,12 +120,12 @@ function branchYear(network: PlanNetwork, line: PlanLine): number | null {
|
||||
network.pieces.find(
|
||||
(piece) =>
|
||||
piece.line === parent.id && atM >= piece.from_m - SAME_M && atM <= piece.to_m + SAME_M,
|
||||
)?.year ?? null
|
||||
) ?? null
|
||||
);
|
||||
}
|
||||
|
||||
/** 선마다 조각이 0 ~ 길이를 빈틈없이 덮게 맞춤 — 경계 = 조각 시작(0 · 끝 근처 빼고) · 연도 = 그 자리 조각 ·
|
||||
* 조각 없는 선 = 한 조각(`year`). 길이 0 선은 조각 없음. */
|
||||
/** 선마다 조각이 0 ~ 길이를 빈틈없이 덮게 맞춤 — 경계 = 조각 시작(0 · 끝 근처 빼고) · 연도 · 세트 = 그 자리 조각 ·
|
||||
* 조각 없는 선 = 한 조각(갈래 = 붙은 자리 세트 · 그 밖 `year` 새 번호) · 연도 · 번호 없는 조각(서버 마크만 · 옛 꼴) = 처음 연도 · 그 해 새 번호. 길이 0 선은 조각 없음. */
|
||||
export function normalizePieces(network: PlanNetwork, year: number): PlanNetwork {
|
||||
const pieces: PlanPiece[] = [];
|
||||
for (const line of network.lines) {
|
||||
@@ -132,22 +136,17 @@ export function normalizePieces(network: PlanNetwork, year: number): PlanNetwork
|
||||
.sort((a, b) => a.from_m - b.from_m);
|
||||
const starts = own
|
||||
.filter((piece) => piece.from_m > MARK_GAP_M / 2 && piece.from_m < length - MARK_GAP_M / 2)
|
||||
.map((piece) => ({ id: piece.id, from: round3(piece.from_m), year: piece.year }));
|
||||
const head = own.find((piece) => piece.from_m <= MARK_GAP_M / 2) ?? {
|
||||
id: "",
|
||||
year: own.length ? year : (branchYear(network, line) ?? year),
|
||||
};
|
||||
const cuts = [{ id: head.id, from: 0, year: head.year }, ...starts];
|
||||
cuts.forEach((cut, i) =>
|
||||
pieces.push({
|
||||
// id 빈 칸 · 연도 없음(서버 마크만 있던 구역) = 새 id · 처음 연도
|
||||
id: cut.id || nextPieceId([...network.pieces, ...pieces]),
|
||||
line: line.id,
|
||||
from_m: cut.from,
|
||||
to_m: i + 1 < cuts.length ? cuts[i + 1].from : length,
|
||||
year: Number.isFinite(cut.year) ? cut.year : year,
|
||||
}),
|
||||
);
|
||||
.map((piece) => ({ ...piece, from_m: round3(piece.from_m) }));
|
||||
const fresh = (own.length ? null : branchPiece(network, line)) ?? { year, set: NaN };
|
||||
const head = own.find((piece) => piece.from_m <= MARK_GAP_M / 2) ?? { ...fresh, id: "" };
|
||||
const cuts = [{ ...head, from_m: 0 }, ...starts];
|
||||
cuts.forEach((cut, i) => {
|
||||
const at = Number.isFinite(cut.year) ? cut.year : year;
|
||||
const set = cut.set > 0 ? cut.set : nextSet([...network.pieces, ...pieces], at);
|
||||
const id = cut.id || nextPieceId([...network.pieces, ...pieces]);
|
||||
const to = i + 1 < cuts.length ? cuts[i + 1].from_m : length;
|
||||
pieces.push({ id, line: line.id, from_m: cut.from_m, to_m: to, year: at, set });
|
||||
});
|
||||
}
|
||||
return { ...network, pieces };
|
||||
}
|
||||
@@ -297,7 +296,7 @@ export function fromServer(
|
||||
(piece) => piece.line === mark.line && Math.abs(piece.from_m - mark.at_m) < 0.01,
|
||||
),
|
||||
)
|
||||
.map((mark) => ({ id: "", line: mark.line, from_m: mark.at_m, to_m: mark.at_m, year: NaN }));
|
||||
.map((mark) => ({ ...mark, id: "", from_m: mark.at_m, to_m: mark.at_m, year: NaN, set: 0 }));
|
||||
return normalizePieces({ lines, pieces: [...pieces, ...cuts] }, year);
|
||||
}
|
||||
|
||||
@@ -497,7 +496,7 @@ export function removeLine(network: PlanNetwork, line: number): PlanNetwork | st
|
||||
};
|
||||
}
|
||||
|
||||
/** `a` 선 꼬리에 `b` 선(머리 = a 꼬리)을 이어 한 선 — 조각 id 그대로 · 이음 자리 두 조각이 같은 해면 하나로. */
|
||||
/** `a` 선 꼬리에 `b` 선(머리 = a 꼬리)을 이어 한 선 — 조각 id 그대로 · 이음 자리 두 조각이 같은 세트면 하나로. */
|
||||
function joinLines(network: PlanNetwork, a: number, b: number): PlanNetwork {
|
||||
const [head, tail] = [network.lines[a], network.lines[b]];
|
||||
const offset = round3(lineLength(head.vertices));
|
||||
@@ -521,7 +520,8 @@ function joinLines(network: PlanNetwork, a: number, b: number): PlanNetwork {
|
||||
!(
|
||||
piece.line === head.id &&
|
||||
Math.abs(piece.from_m - offset) < SAME_M &&
|
||||
last?.year === piece.year
|
||||
last?.year === piece.year &&
|
||||
last.set === piece.set
|
||||
),
|
||||
);
|
||||
const lines = network.lines
|
||||
|
||||
@@ -125,7 +125,14 @@ export function segmentsInBox(
|
||||
})),
|
||||
pieces: segs.map((seg, i) => {
|
||||
const [a, b] = segmentPath(network, seg)!;
|
||||
return { id: String(i), line: `s${i}`, from_m: 0, to_m: segmentLength(a, b), year: 0 };
|
||||
return {
|
||||
id: String(i),
|
||||
line: `s${i}`,
|
||||
from_m: 0,
|
||||
to_m: segmentLength(a, b),
|
||||
year: 0,
|
||||
set: 0,
|
||||
};
|
||||
}),
|
||||
};
|
||||
return piecesInBox(probe, toView, box, crossing).map((i) => segs[Number(i)]);
|
||||
|
||||
@@ -0,0 +1,533 @@
|
||||
{
|
||||
"about": "PLAN 638 세트 합침 거울 사례 — 서버 Network.derive · 화면 Group.mergeSets(fromServer 뒤) 같은 답. 입력 = PUT 꼴(lines · marks · pieces) · 기대 = (선 차례, from_m) 차례 조각.",
|
||||
"year_start": 2027,
|
||||
"cases": [
|
||||
{
|
||||
"name": "같은 선 이웃 같은 세트 = 하나(앞 id)",
|
||||
"network": {
|
||||
"lines": [
|
||||
{
|
||||
"id": "main",
|
||||
"vertices": [
|
||||
[0, 0],
|
||||
[100, 0]
|
||||
],
|
||||
"branch": null
|
||||
}
|
||||
],
|
||||
"marks": [
|
||||
{
|
||||
"line": "main",
|
||||
"at_m": 30
|
||||
},
|
||||
{
|
||||
"line": "main",
|
||||
"at_m": 60
|
||||
}
|
||||
],
|
||||
"pieces": [
|
||||
{
|
||||
"id": "p1",
|
||||
"line": "main",
|
||||
"from_m": 0,
|
||||
"to_m": 30,
|
||||
"year": 2027,
|
||||
"set": 1
|
||||
},
|
||||
{
|
||||
"id": "p2",
|
||||
"line": "main",
|
||||
"from_m": 30,
|
||||
"to_m": 60,
|
||||
"year": 2027,
|
||||
"set": 1
|
||||
},
|
||||
{
|
||||
"id": "p3",
|
||||
"line": "main",
|
||||
"from_m": 60,
|
||||
"to_m": 100,
|
||||
"year": 2027,
|
||||
"set": 2
|
||||
}
|
||||
]
|
||||
},
|
||||
"expect": [
|
||||
{
|
||||
"id": "p1",
|
||||
"line": "main",
|
||||
"from_m": 0,
|
||||
"to_m": 60,
|
||||
"year": 2027,
|
||||
"set": 1
|
||||
},
|
||||
{
|
||||
"id": "p3",
|
||||
"line": "main",
|
||||
"from_m": 60,
|
||||
"to_m": 100,
|
||||
"year": 2027,
|
||||
"set": 2
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "셋 이어 합침 · 앞 조각 다른 세트 그대로",
|
||||
"network": {
|
||||
"lines": [
|
||||
{
|
||||
"id": "main",
|
||||
"vertices": [
|
||||
[0, 0],
|
||||
[100, 0]
|
||||
],
|
||||
"branch": null
|
||||
}
|
||||
],
|
||||
"marks": [
|
||||
{
|
||||
"line": "main",
|
||||
"at_m": 20
|
||||
},
|
||||
{
|
||||
"line": "main",
|
||||
"at_m": 50
|
||||
},
|
||||
{
|
||||
"line": "main",
|
||||
"at_m": 80
|
||||
}
|
||||
],
|
||||
"pieces": [
|
||||
{
|
||||
"id": "p1",
|
||||
"line": "main",
|
||||
"from_m": 0,
|
||||
"to_m": 20,
|
||||
"year": 2027,
|
||||
"set": 1
|
||||
},
|
||||
{
|
||||
"id": "p4",
|
||||
"line": "main",
|
||||
"from_m": 20,
|
||||
"to_m": 50,
|
||||
"year": 2028,
|
||||
"set": 2
|
||||
},
|
||||
{
|
||||
"id": "p2",
|
||||
"line": "main",
|
||||
"from_m": 50,
|
||||
"to_m": 80,
|
||||
"year": 2028,
|
||||
"set": 2
|
||||
},
|
||||
{
|
||||
"id": "p3",
|
||||
"line": "main",
|
||||
"from_m": 80,
|
||||
"to_m": 100,
|
||||
"year": 2028,
|
||||
"set": 2
|
||||
}
|
||||
]
|
||||
},
|
||||
"expect": [
|
||||
{
|
||||
"id": "p1",
|
||||
"line": "main",
|
||||
"from_m": 0,
|
||||
"to_m": 20,
|
||||
"year": 2027,
|
||||
"set": 1
|
||||
},
|
||||
{
|
||||
"id": "p4",
|
||||
"line": "main",
|
||||
"from_m": 20,
|
||||
"to_m": 100,
|
||||
"year": 2028,
|
||||
"set": 2
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "같은 해 다른 번호 · 다른 해 같은 번호 = 안 합침",
|
||||
"network": {
|
||||
"lines": [
|
||||
{
|
||||
"id": "main",
|
||||
"vertices": [
|
||||
[0, 0],
|
||||
[90, 0]
|
||||
],
|
||||
"branch": null
|
||||
}
|
||||
],
|
||||
"marks": [
|
||||
{
|
||||
"line": "main",
|
||||
"at_m": 30
|
||||
},
|
||||
{
|
||||
"line": "main",
|
||||
"at_m": 60
|
||||
}
|
||||
],
|
||||
"pieces": [
|
||||
{
|
||||
"id": "p1",
|
||||
"line": "main",
|
||||
"from_m": 0,
|
||||
"to_m": 30,
|
||||
"year": 2027,
|
||||
"set": 1
|
||||
},
|
||||
{
|
||||
"id": "p2",
|
||||
"line": "main",
|
||||
"from_m": 30,
|
||||
"to_m": 60,
|
||||
"year": 2027,
|
||||
"set": 2
|
||||
},
|
||||
{
|
||||
"id": "p3",
|
||||
"line": "main",
|
||||
"from_m": 60,
|
||||
"to_m": 90,
|
||||
"year": 2028,
|
||||
"set": 2
|
||||
}
|
||||
]
|
||||
},
|
||||
"expect": [
|
||||
{
|
||||
"id": "p1",
|
||||
"line": "main",
|
||||
"from_m": 0,
|
||||
"to_m": 30,
|
||||
"year": 2027,
|
||||
"set": 1
|
||||
},
|
||||
{
|
||||
"id": "p2",
|
||||
"line": "main",
|
||||
"from_m": 30,
|
||||
"to_m": 60,
|
||||
"year": 2027,
|
||||
"set": 2
|
||||
},
|
||||
{
|
||||
"id": "p3",
|
||||
"line": "main",
|
||||
"from_m": 60,
|
||||
"to_m": 90,
|
||||
"year": 2028,
|
||||
"set": 2
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "다른 선 같은 세트 = 안 합침(갈래 · 떨어진 선)",
|
||||
"network": {
|
||||
"lines": [
|
||||
{
|
||||
"id": "main",
|
||||
"vertices": [
|
||||
[0, 0],
|
||||
[50, 0],
|
||||
[100, 0]
|
||||
],
|
||||
"branch": null
|
||||
},
|
||||
{
|
||||
"id": "branch-1",
|
||||
"vertices": [
|
||||
[50, 0],
|
||||
[50, 40]
|
||||
],
|
||||
"branch": {
|
||||
"line": "main",
|
||||
"vertex": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "line-2",
|
||||
"vertices": [
|
||||
[100, 0],
|
||||
[150, 0]
|
||||
],
|
||||
"branch": null
|
||||
}
|
||||
],
|
||||
"marks": [],
|
||||
"pieces": [
|
||||
{
|
||||
"id": "p1",
|
||||
"line": "main",
|
||||
"from_m": 0,
|
||||
"to_m": 100,
|
||||
"year": 2027,
|
||||
"set": 1
|
||||
},
|
||||
{
|
||||
"id": "p2",
|
||||
"line": "branch-1",
|
||||
"from_m": 0,
|
||||
"to_m": 40,
|
||||
"year": 2027,
|
||||
"set": 1
|
||||
},
|
||||
{
|
||||
"id": "p3",
|
||||
"line": "line-2",
|
||||
"from_m": 0,
|
||||
"to_m": 50,
|
||||
"year": 2027,
|
||||
"set": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
"expect": [
|
||||
{
|
||||
"id": "p1",
|
||||
"line": "main",
|
||||
"from_m": 0,
|
||||
"to_m": 100,
|
||||
"year": 2027,
|
||||
"set": 1
|
||||
},
|
||||
{
|
||||
"id": "p2",
|
||||
"line": "branch-1",
|
||||
"from_m": 0,
|
||||
"to_m": 40,
|
||||
"year": 2027,
|
||||
"set": 1
|
||||
},
|
||||
{
|
||||
"id": "p3",
|
||||
"line": "line-2",
|
||||
"from_m": 0,
|
||||
"to_m": 50,
|
||||
"year": 2027,
|
||||
"set": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "옛 꼴(set 없음) = 선 · from_m 차례로 그 해 1 · 2 …",
|
||||
"network": {
|
||||
"lines": [
|
||||
{
|
||||
"id": "main",
|
||||
"vertices": [
|
||||
[0, 0],
|
||||
[100, 0]
|
||||
],
|
||||
"branch": null
|
||||
},
|
||||
{
|
||||
"id": "line-1",
|
||||
"vertices": [
|
||||
[0, 50],
|
||||
[80, 50]
|
||||
],
|
||||
"branch": null
|
||||
}
|
||||
],
|
||||
"marks": [
|
||||
{
|
||||
"line": "main",
|
||||
"at_m": 40
|
||||
},
|
||||
{
|
||||
"line": "main",
|
||||
"at_m": 70
|
||||
}
|
||||
],
|
||||
"pieces": [
|
||||
{
|
||||
"id": "p1",
|
||||
"line": "main",
|
||||
"from_m": 0,
|
||||
"to_m": 40,
|
||||
"year": 2027
|
||||
},
|
||||
{
|
||||
"id": "p2",
|
||||
"line": "main",
|
||||
"from_m": 40,
|
||||
"to_m": 70,
|
||||
"year": 2028
|
||||
},
|
||||
{
|
||||
"id": "p4",
|
||||
"line": "main",
|
||||
"from_m": 70,
|
||||
"to_m": 100,
|
||||
"year": 2027
|
||||
},
|
||||
{
|
||||
"id": "p3",
|
||||
"line": "line-1",
|
||||
"from_m": 0,
|
||||
"to_m": 80,
|
||||
"year": 2027
|
||||
}
|
||||
]
|
||||
},
|
||||
"expect": [
|
||||
{
|
||||
"id": "p1",
|
||||
"line": "main",
|
||||
"from_m": 0,
|
||||
"to_m": 40,
|
||||
"year": 2027,
|
||||
"set": 1
|
||||
},
|
||||
{
|
||||
"id": "p2",
|
||||
"line": "main",
|
||||
"from_m": 40,
|
||||
"to_m": 70,
|
||||
"year": 2028,
|
||||
"set": 1
|
||||
},
|
||||
{
|
||||
"id": "p4",
|
||||
"line": "main",
|
||||
"from_m": 70,
|
||||
"to_m": 100,
|
||||
"year": 2027,
|
||||
"set": 2
|
||||
},
|
||||
{
|
||||
"id": "p3",
|
||||
"line": "line-1",
|
||||
"from_m": 0,
|
||||
"to_m": 80,
|
||||
"year": 2027,
|
||||
"set": 3
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "set 빠진 조각 하나 = 그 해 최댓값 + 1",
|
||||
"network": {
|
||||
"lines": [
|
||||
{
|
||||
"id": "main",
|
||||
"vertices": [
|
||||
[0, 0],
|
||||
[100, 0]
|
||||
],
|
||||
"branch": null
|
||||
}
|
||||
],
|
||||
"marks": [
|
||||
{
|
||||
"line": "main",
|
||||
"at_m": 50
|
||||
}
|
||||
],
|
||||
"pieces": [
|
||||
{
|
||||
"id": "p1",
|
||||
"line": "main",
|
||||
"from_m": 0,
|
||||
"to_m": 50,
|
||||
"year": 2027,
|
||||
"set": 3
|
||||
},
|
||||
{
|
||||
"id": "p2",
|
||||
"line": "main",
|
||||
"from_m": 50,
|
||||
"to_m": 100,
|
||||
"year": 2027
|
||||
}
|
||||
]
|
||||
},
|
||||
"expect": [
|
||||
{
|
||||
"id": "p1",
|
||||
"line": "main",
|
||||
"from_m": 0,
|
||||
"to_m": 50,
|
||||
"year": 2027,
|
||||
"set": 3
|
||||
},
|
||||
{
|
||||
"id": "p2",
|
||||
"line": "main",
|
||||
"from_m": 50,
|
||||
"to_m": 100,
|
||||
"year": 2027,
|
||||
"set": 4
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "최댓값 = 그 해 모든 조각(뒤 선 것 포함)",
|
||||
"network": {
|
||||
"lines": [
|
||||
{
|
||||
"id": "main",
|
||||
"vertices": [
|
||||
[0, 0],
|
||||
[100, 0]
|
||||
],
|
||||
"branch": null
|
||||
},
|
||||
{
|
||||
"id": "line-1",
|
||||
"vertices": [
|
||||
[0, 50],
|
||||
[80, 50]
|
||||
],
|
||||
"branch": null
|
||||
}
|
||||
],
|
||||
"marks": [],
|
||||
"pieces": [
|
||||
{
|
||||
"id": "p1",
|
||||
"line": "main",
|
||||
"from_m": 0,
|
||||
"to_m": 100,
|
||||
"year": 2027
|
||||
},
|
||||
{
|
||||
"id": "p2",
|
||||
"line": "line-1",
|
||||
"from_m": 0,
|
||||
"to_m": 80,
|
||||
"year": 2027,
|
||||
"set": 2
|
||||
}
|
||||
]
|
||||
},
|
||||
"expect": [
|
||||
{
|
||||
"id": "p1",
|
||||
"line": "main",
|
||||
"from_m": 0,
|
||||
"to_m": 100,
|
||||
"year": 2027,
|
||||
"set": 3
|
||||
},
|
||||
{
|
||||
"id": "p2",
|
||||
"line": "line-1",
|
||||
"from_m": 0,
|
||||
"to_m": 80,
|
||||
"year": 2027,
|
||||
"set": 2
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,298 @@
|
||||
"""D03 예정노선 노선 세트 화면 셈(PLAN 638-2 · 요구 547 · 548) — TS `logic/D03_PlanAnalysis_Group.ts` · `Plan.ts` 세트 칸.
|
||||
|
||||
tsc 로 옮겨 Node 로 돌림.
|
||||
- 거울: `fixtures/d03_638_set_cases.json` 사례마다 fromServer → mergeSets = 기대 조각(서버 `Network.derive` 와 같은 답).
|
||||
- 세트 지정: 고른 구간 끝에서 나눔 · 되돌려 지정하면 하나로(앞 id) · 경계 1 m 안 = 그 경계 · 두 선 구간.
|
||||
- 고를 세트: 있는 세트 + 해마다 새 번호(최댓값 + 1) · 지워도 다른 번호 그대로.
|
||||
- 새 선 = 사업연도 시작 해 새 번호 · 갈래 = 붙은 자리 세트 · 이어 긋기 = 끝 세트 그대로.
|
||||
- [잇기]: 닿음 · 떨어짐(덧 토막 = 앞 선 세트) · 머리끼리 · 같은 선 · 갈래 첫 점 = 까닭 글.
|
||||
- 기존 동작: 마크 · 토막 지움 · 방향 바꿈에 세트가 따라감 · 이어 긋기 이음 자리 = 같은 세트만 합침.
|
||||
|
||||
./venv/Scripts/python.exe -m pytest resources/tester/test_638_group.py -q
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
LOGIC = ROOT / "D03_PlanAnalysis" / "logic"
|
||||
TS_FILES = [LOGIC / f"D03_PlanAnalysis_{name}.ts" for name in ("Draw", "Plan", "Select", "Group")]
|
||||
TSC = ROOT / "config" / "node_modules" / "typescript" / "bin" / "tsc"
|
||||
CASES = Path(__file__).resolve().parent / "fixtures" / "d03_638_set_cases.json"
|
||||
|
||||
_RUNNER = """
|
||||
import { readFileSync, writeFileSync } from "node:fs";
|
||||
import * as plan from "./D03_PlanAnalysis/logic/D03_PlanAnalysis_Plan.js";
|
||||
import * as group from "./D03_PlanAnalysis/logic/D03_PlanAnalysis_Group.js";
|
||||
import { removeSegment } from "./D03_PlanAnalysis/logic/D03_PlanAnalysis_Select.js";
|
||||
const Y = 2027;
|
||||
const view = (net) => (typeof net === "string" ? net : {
|
||||
lines: net.lines.map((l) => ({ id: l.id, vertices: l.vertices, branch: l.branch })),
|
||||
pieces: net.pieces.map(({ id, line, from_m, to_m, year, set }) => ({ id, line, from_m, to_m, year, set })),
|
||||
});
|
||||
const piece = (id, line, from_m, to_m, year, set) => ({ id, line, from_m, to_m, year, set });
|
||||
const line = (id, vertices, branch = null) => ({ id, vertices, branch });
|
||||
const out = {};
|
||||
|
||||
// 거울
|
||||
const fixture = JSON.parse(readFileSync(process.argv[3], "utf-8"));
|
||||
out.mirror = fixture.cases.map((c) => {
|
||||
const n = c.network;
|
||||
return view(group.mergeSets(plan.fromServer(n.lines, n.marks, n.pieces, fixture.year_start))).pieces;
|
||||
});
|
||||
|
||||
// 세트 지정
|
||||
const one = { lines: [line("main", [[0, 0], [100, 0]])], pieces: [piece("p1", "main", 0, 100, Y, 1)] };
|
||||
const assigned = group.assignSet(one, [{ line: "main", from_m: 30, to_m: 60 }], { year: Y, set: 2 });
|
||||
out.assign = view(assigned);
|
||||
out.assignBack = view(group.assignSet(assigned, [{ line: "main", from_m: 60, to_m: 30 }], { year: Y, set: 1 }));
|
||||
out.assignNear = view(group.assignSet(assigned, [{ line: "main", from_m: 0.5, to_m: 30.4 }], { year: 2028, set: 1 }));
|
||||
const two = {
|
||||
lines: [line("main", [[0, 0], [100, 0]]), line("line-1", [[0, 50], [100, 50]])],
|
||||
pieces: [piece("p1", "main", 0, 100, Y, 1), piece("p2", "line-1", 0, 100, Y, 2)],
|
||||
};
|
||||
out.assignTwo = view(group.assignSet(two, [
|
||||
{ line: "main", from_m: 50, to_m: 100 },
|
||||
{ line: "line-1", from_m: 0, to_m: 40 },
|
||||
], { year: 2028, set: 1 }));
|
||||
// 마크만 놓인 다른 선은 지정에 안 걷힘
|
||||
const marked = plan.splitPiece(two, "line-1", 70);
|
||||
out.assignKeepsOtherMark = view(group.assignSet(marked, [{ line: "main", from_m: 0, to_m: 20 }], { year: Y, set: 3 }));
|
||||
|
||||
// 고를 세트
|
||||
const many = {
|
||||
lines: [line("main", [[0, 0], [300, 0]])],
|
||||
pieces: [
|
||||
piece("p1", "main", 0, 100, Y, 1), piece("p2", "main", 100, 200, Y, 3), piece("p3", "main", 200, 300, 2028, 1),
|
||||
],
|
||||
};
|
||||
out.choices = group.setChoices(many, [2027, 2028, 2029]).map((c) => [c.key, c.fresh]);
|
||||
const dropped = plan.removeMark(many, "main", 100, 0.1); // 2027-3 사라짐 = 다른 번호 그대로
|
||||
out.choicesAfterDrop = group.setChoices(dropped, [2027]).map((c) => [c.key, c.fresh]);
|
||||
out.key = group.setKey({ year: 2027, set: 2 });
|
||||
|
||||
// 새 선 · 갈래 · 이어 긋기
|
||||
let net = plan.normalizePieces(plan.appendVertex(plan.newLine(one, [0, 80]), 1, [50, 80]), Y);
|
||||
out.newLine = view(net).pieces;
|
||||
const yearly = {
|
||||
lines: [line("main", [[0, 0], [50, 0], [100, 0]])],
|
||||
pieces: [piece("p1", "main", 0, 25, Y, 1), piece("p2", "main", 25, 100, 2028, 4)],
|
||||
};
|
||||
net = plan.normalizePieces(plan.appendVertex(plan.addBranch(yearly, 0, 1), 1, [50, 40]), Y);
|
||||
out.branch = view(net).pieces;
|
||||
out.extend = view(plan.normalizePieces(plan.appendVertex(yearly, 0, [150, 0]), Y)).pieces;
|
||||
out.split = view(plan.splitPiece(one, "main", 40)).pieces;
|
||||
out.reverse = view(plan.reverseLine(yearly, 0)).pieces;
|
||||
out.cutMiddle = view(plan.normalizePieces(removeSegment(yearly, { line: "main", index: 0 }), Y)).pieces;
|
||||
const mid = { lines: [line("main", [[0, 0], [30, 0], [60, 0], [100, 0]])], pieces: [piece("p1", "main", 0, 100, Y, 1)] };
|
||||
out.cutInner = view(plan.normalizePieces(removeSegment(mid, { line: "main", index: 1 }), Y));
|
||||
|
||||
// 이어 긋기 이음(attachHead) — 같은 세트 = 하나 · 다른 세트 = 둘
|
||||
const pair = (setB) => ({
|
||||
lines: [line("main", [[0, 0], [50, 0]]), line("line-1", [[50, 0], [100, 0]])],
|
||||
pieces: [piece("p1", "main", 0, 50, Y, 1), piece("p2", "line-1", 0, 50, Y, setB)],
|
||||
});
|
||||
out.attachSame = view(plan.normalizePieces(plan.attachHead(pair(1), 1), Y));
|
||||
out.attachOther = view(plan.normalizePieces(plan.attachHead(pair(2), 1), Y));
|
||||
|
||||
// [잇기]
|
||||
out.joinTouch = view(group.joinEnds(pair(2), { line: "main", head: false }, { line: "line-1", head: true }, Y));
|
||||
out.joinTouchSame = view(group.joinEnds(pair(1), { line: "line-1", head: true }, { line: "main", head: false }, Y));
|
||||
const gap = {
|
||||
lines: [line("main", [[0, 0], [50, 0]]), line("line-1", [[60, 0], [100, 0]])],
|
||||
pieces: [piece("p1", "main", 0, 50, Y, 1), piece("p2", "line-1", 0, 40, Y, 2)],
|
||||
};
|
||||
out.joinGapA = view(group.joinEnds(gap, { line: "main", head: false }, { line: "line-1", head: true }, Y));
|
||||
out.joinGapB = view(group.joinEnds(gap, { line: "line-1", head: true }, { line: "main", head: false }, Y));
|
||||
const heads = {
|
||||
lines: [line("main", [[0, 0], [-50, 0]]), line("line-1", [[0, 0], [50, 0]])],
|
||||
pieces: [piece("p1", "main", 0, 50, Y, 1), piece("p2", "line-1", 0, 50, 2028, 1)],
|
||||
};
|
||||
out.joinHeads = view(group.joinEnds(heads, { line: "main", head: true }, { line: "line-1", head: true }, Y));
|
||||
const tails = {
|
||||
lines: [line("main", [[0, 0], [50, 0]]), line("line-1", [[100, 0], [60, 0]])],
|
||||
pieces: [piece("p1", "main", 0, 50, Y, 1), piece("p2", "line-1", 0, 40, Y, 2)],
|
||||
};
|
||||
out.joinTails = view(group.joinEnds(tails, { line: "main", head: false }, { line: "line-1", head: false }, Y));
|
||||
out.joinSame = group.joinEnds(pair(1), { line: "main", head: true }, { line: "main", head: false }, Y);
|
||||
const withBranch = plan.normalizePieces(plan.appendVertex(plan.addBranch(yearly, 0, 1), 1, [50, 40]), Y);
|
||||
out.joinBranchHead = group.joinEnds(withBranch, { line: "branch-1", head: true }, { line: "main", head: false }, Y);
|
||||
out.joinBranchTail = view(group.joinEnds(
|
||||
{ ...withBranch, lines: [...withBranch.lines, line("line-2", [[50, 40], [50, 90]])],
|
||||
pieces: [...withBranch.pieces, piece("p9", "line-2", 0, 50, 2029, 1)] },
|
||||
{ line: "branch-1", head: false }, { line: "line-2", head: true }, Y));
|
||||
writeFileSync(process.argv[2], JSON.stringify(out));
|
||||
"""
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def r(tmp_path_factory: pytest.TempPathFactory) -> dict:
|
||||
tmp = tmp_path_factory.mktemp("group")
|
||||
out = tmp / "js"
|
||||
subprocess.run( # noqa: S603 — 고정 실행 파일
|
||||
["node", str(TSC), *map(str, TS_FILES), "--outDir", str(out), "--rootDir", str(ROOT)]
|
||||
+ ["--module", "esnext", "--target", "es2022", "--moduleResolution", "bundler"]
|
||||
+ ["--ignoreConfig"],
|
||||
cwd=str(ROOT),
|
||||
check=True,
|
||||
capture_output=True,
|
||||
)
|
||||
for emitted in out.rglob("*.js"):
|
||||
emitted.write_text(
|
||||
re.sub(r'(from\s+"\.\.?/[^"]+)"', r'\1.js"', emitted.read_text(encoding="utf-8")),
|
||||
encoding="utf-8",
|
||||
)
|
||||
(out / "runner.mjs").write_text(_RUNNER, encoding="utf-8")
|
||||
result = tmp / "out.json"
|
||||
subprocess.run( # noqa: S603
|
||||
["node", str(out / "runner.mjs"), str(result), str(CASES)],
|
||||
cwd=str(ROOT),
|
||||
check=True,
|
||||
capture_output=True,
|
||||
)
|
||||
return json.loads(result.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def _span(pieces: list[dict]) -> list[tuple]:
|
||||
return [(p["id"], p["line"], p["from_m"], p["to_m"], p["year"], p["set"]) for p in pieces]
|
||||
|
||||
|
||||
def test_거울_사례_서버와_같은_조각(r):
|
||||
cases = json.loads(CASES.read_text(encoding="utf-8"))["cases"]
|
||||
assert len(cases) == len(r["mirror"]) >= 6
|
||||
for case, got in zip(cases, r["mirror"]):
|
||||
assert _span(got) == _span(case["expect"]), case["name"]
|
||||
|
||||
|
||||
def test_세트_지정_끝에서_나눔_되돌리면_하나(r):
|
||||
assert _span(r["assign"]["pieces"]) == [
|
||||
("p1", "main", 0, 30, 2027, 1),
|
||||
("p2", "main", 30, 60, 2027, 2),
|
||||
("p3", "main", 60, 100, 2027, 1),
|
||||
]
|
||||
# 거꾸로 고른 구간도 같음 · 셋이 한 세트 = 앞 조각 id 하나(마크 걷음)
|
||||
assert _span(r["assignBack"]["pieces"]) == [("p1", "main", 0, 100, 2027, 1)]
|
||||
|
||||
|
||||
def test_경계_1m_안은_그_경계(r):
|
||||
assert _span(r["assignNear"]["pieces"]) == [
|
||||
("p1", "main", 0, 30, 2028, 1),
|
||||
("p2", "main", 30, 60, 2027, 2),
|
||||
("p3", "main", 60, 100, 2027, 1),
|
||||
]
|
||||
|
||||
|
||||
def test_두_선_구간_지정_다른_선_마크는_그대로(r):
|
||||
assert _span(r["assignTwo"]["pieces"]) == [
|
||||
("p1", "main", 0, 50, 2027, 1),
|
||||
("p3", "main", 50, 100, 2028, 1),
|
||||
("p2", "line-1", 0, 40, 2028, 1),
|
||||
("p4", "line-1", 40, 100, 2027, 2),
|
||||
]
|
||||
other = [p for p in r["assignKeepsOtherMark"]["pieces"] if p["line"] == "line-1"]
|
||||
assert [(p["from_m"], p["to_m"]) for p in other] == [(0, 70), (70, 100)]
|
||||
|
||||
|
||||
def test_고를_세트_새_번호(r):
|
||||
assert r["key"] == "2027-2"
|
||||
assert r["choices"] == [
|
||||
["2027-1", False],
|
||||
["2027-3", False],
|
||||
["2027-4", True],
|
||||
["2028-1", False],
|
||||
["2028-2", True],
|
||||
["2029-1", True],
|
||||
]
|
||||
# 2027-3 이 사라져도 2027-1 그대로 · 새 번호 = 남은 최댓값 + 1
|
||||
assert r["choicesAfterDrop"] == [
|
||||
["2027-1", False],
|
||||
["2027-2", True],
|
||||
["2028-1", False],
|
||||
["2028-2", True],
|
||||
] # 넘긴 해 밖이라도 있는 세트의 해는 목록에
|
||||
|
||||
|
||||
def test_새_선_사업연도_새_번호_갈래_붙은_자리_이어_긋기(r):
|
||||
assert [(p["line"], p["year"], p["set"]) for p in r["newLine"]] == [
|
||||
("main", 2027, 1),
|
||||
("line-1", 2027, 2),
|
||||
]
|
||||
branch = [p for p in r["branch"] if p["line"] == "branch-1"]
|
||||
assert [(p["year"], p["set"]) for p in branch] == [(2028, 4)] # 붙은 자리(50 m) = p2
|
||||
assert _span(r["extend"]) == [("p1", "main", 0, 25, 2027, 1), ("p2", "main", 25, 150, 2028, 4)]
|
||||
|
||||
|
||||
def test_기존_동작_세트가_따라감(r):
|
||||
assert [(p["id"], p["set"]) for p in r["split"]] == [("p1", 1), ("p2", 1)]
|
||||
assert _span(r["reverse"]) == [("p1", "main", 75, 100, 2027, 1), ("p2", "main", 0, 75, 2028, 4)]
|
||||
assert _span(r["cutMiddle"]) == [("p2", "main", 0, 50, 2028, 4)]
|
||||
inner = r["cutInner"]
|
||||
assert [line["id"] for line in inner["lines"]] == ["main", "line-1"]
|
||||
assert [(p["line"], p["year"], p["set"]) for p in inner["pieces"]] == [
|
||||
("main", 2027, 1),
|
||||
("line-1", 2027, 1),
|
||||
] # 갈라져도 같은 세트
|
||||
|
||||
|
||||
def test_이어_긋기_이음은_같은_세트만_합침(r):
|
||||
assert _span(r["attachSame"]["pieces"]) == [("p1", "main", 0, 100, 2027, 1)]
|
||||
assert _span(r["attachOther"]["pieces"]) == [
|
||||
("p1", "main", 0, 50, 2027, 1),
|
||||
("p2", "main", 50, 100, 2027, 2),
|
||||
]
|
||||
|
||||
|
||||
def test_잇기_닿음(r):
|
||||
one = r["joinTouch"]
|
||||
assert [line["id"] for line in one["lines"]] == ["main"]
|
||||
assert one["lines"][0]["vertices"] == [[0, 0], [50, 0], [100, 0]]
|
||||
assert _span(one["pieces"]) == [
|
||||
("p1", "main", 0, 50, 2027, 1),
|
||||
("p2", "main", 50, 100, 2027, 2),
|
||||
]
|
||||
assert _span(r["joinTouchSame"]["pieces"]) == [("p1", "main", 0, 100, 2027, 1)]
|
||||
|
||||
|
||||
def test_잇기_떨어짐_덧_토막_앞_선_세트(r):
|
||||
a = r["joinGapA"] # 앞 = main 꼬리 → 덧 토막 50~60 = 2027-1
|
||||
assert a["lines"][0]["vertices"] == [[0, 0], [50, 0], [60, 0], [100, 0]]
|
||||
assert _span(a["pieces"]) == [("p1", "main", 0, 60, 2027, 1), ("p2", "main", 60, 100, 2027, 2)]
|
||||
b = r["joinGapB"] # 앞 = line-1 머리 → 덧 토막 = 2027-2
|
||||
assert len(b["lines"]) == 1 and b["lines"][0]["vertices"][0] == [0, 0]
|
||||
assert [(p["from_m"], p["to_m"], p["set"]) for p in b["pieces"]] == [(0, 50, 1), (50, 100, 2)]
|
||||
|
||||
|
||||
def test_잇기_머리끼리_꼬리끼리(r):
|
||||
heads = r["joinHeads"]
|
||||
assert len(heads["lines"]) == 1 and len(heads["lines"][0]["vertices"]) == 3
|
||||
assert [(p["year"], p["set"], p["to_m"] - p["from_m"]) for p in heads["pieces"]] == [
|
||||
(2027, 1, 50),
|
||||
(2028, 1, 50),
|
||||
]
|
||||
tails = r["joinTails"]
|
||||
assert tails["lines"][0]["vertices"] == [[0, 0], [50, 0], [60, 0], [100, 0]]
|
||||
assert [(p["from_m"], p["to_m"], p["set"]) for p in tails["pieces"]] == [
|
||||
(0, 60, 1),
|
||||
(60, 100, 2),
|
||||
]
|
||||
|
||||
|
||||
def test_잇기_못_함_까닭_글_갈래_꼬리는_됨(r):
|
||||
assert "같은 선" in r["joinSame"]
|
||||
assert "갈래 첫 점" in r["joinBranchHead"]
|
||||
tail = r["joinBranchTail"]
|
||||
ids = [line["id"] for line in tail["lines"]]
|
||||
assert "line-2" not in ids and "branch-1" in ids
|
||||
branch = next(line for line in tail["lines"] if line["id"] == "branch-1")
|
||||
assert branch["branch"] == {"line": "main", "vertex": 1}
|
||||
assert branch["vertices"] == [[50, 0], [50, 40], [50, 90]]
|
||||
assert [(p["year"], p["set"]) for p in tail["pieces"] if p["line"] == "branch-1"] == [
|
||||
(2028, 4),
|
||||
(2029, 1),
|
||||
]
|
||||
Reference in New Issue
Block a user