Merge remote-tracking branch 'origin/sub_laptop_7' into main_laptop_1
This commit is contained in:
@@ -9,7 +9,13 @@ import { currentLanguageIndex } from "@ui/ui_template_locale";
|
||||
import { parseA1, toA1 } from "../../A00_Common/spreadsheet/spreadsheet_address";
|
||||
import { recalcWorkbook } from "../../A00_Common/spreadsheet/spreadsheet_recalc";
|
||||
import type { CalcValue, Cell } from "../../A00_Common/spreadsheet/spreadsheet_types";
|
||||
import { nodeSheet, type SheetLogic, type SlNode, type SlOutside } from "./M01_SL_Model";
|
||||
import {
|
||||
nodeSheet,
|
||||
type SheetLogic,
|
||||
type SlNode,
|
||||
type SlOutside,
|
||||
type SlSource,
|
||||
} from "./M01_SL_Model";
|
||||
|
||||
/** 알약 값 — 수 · 글 · 참거짓 · 빈 칸 */
|
||||
export type SlValue = number | string | boolean | null;
|
||||
@@ -48,6 +54,11 @@ const TEXT = {
|
||||
"Outside value 「{pill}」 of 「{box}」 not filled yet",
|
||||
],
|
||||
NoResult: ["결과 상자가 없음", "There is no result box"],
|
||||
/** {cell} {why} */
|
||||
NoAnswer: [
|
||||
"{cell} 칸 바깥 값을 못 받음 {why}",
|
||||
"Couldn't get the outside value for {cell} {why}",
|
||||
],
|
||||
NoLine: ["결과 상자에 이어진 선이 없음", "No line reaches the result box"],
|
||||
} as const;
|
||||
|
||||
@@ -63,13 +74,48 @@ const tl = (key: keyof typeof TEXT, fill: Record<string, string> = {}): string =
|
||||
export function outsideCells(sl: SheetLogic): SlOutside[] {
|
||||
const kindOf = (n: SlNode): SlOutside["kind"] =>
|
||||
n.kind === "table" ? "table" : n.kind === "logic" ? "logic" : "price";
|
||||
const sourceOf = (n: SlNode): SlSource | undefined =>
|
||||
n.source ??
|
||||
(n.ref && n.kind === "table" ? { kind: "표", table: n.ref } : undefined) ??
|
||||
(n.ref && n.kind === "logic" ? { kind: "로직", key: n.ref, inputs: {} } : undefined);
|
||||
return sl.nodes.flatMap((n) =>
|
||||
[...n.pillsIn, ...n.pillsOut]
|
||||
.filter((p) => p.outside)
|
||||
.map((p) => ({ node: n.id, cell: p.cell, kind: kindOf(n), ref: n.ref ?? "" })),
|
||||
.map((p) => {
|
||||
const source = sourceOf(n);
|
||||
const at = { id: `${n.id}!${p.cell}`, node: n.id, cell: p.cell, kind: kindOf(n) };
|
||||
return { ...at, ref: n.ref ?? "", ...(source ? { source } : {}) };
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/** 서버 바깥 값 답 한 줄(`/api/m01/sl/outside`) */
|
||||
export interface SlOutsideAnswer {
|
||||
id: string;
|
||||
ok: boolean;
|
||||
value?: number | null;
|
||||
rows?: (string | number | null)[][];
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
/** 서버 답 → 넣을 값 · 못 받은 것은 쉬운 말 */
|
||||
export function answersToValues(
|
||||
cells: SlOutside[],
|
||||
answers: SlOutsideAnswer[],
|
||||
): { values: SlOutsideValue[]; errors: string[] } {
|
||||
const values: SlOutsideValue[] = [];
|
||||
const errors: string[] = [];
|
||||
for (const c of cells) {
|
||||
const a = answers.find((x) => x.id === c.id);
|
||||
if (!a || !a.ok) {
|
||||
errors.push(tl("NoAnswer", { cell: c.cell, why: a?.reason ?? "" }));
|
||||
continue;
|
||||
}
|
||||
values.push({ node: c.node, cell: c.cell, value: a.rows ?? a.value ?? null });
|
||||
}
|
||||
return { values, errors };
|
||||
}
|
||||
|
||||
/** 칸 하나에 값 — null 이면 값을 뺌(서식은 둠) */
|
||||
function putValue(칸: Record<string, Cell>, cell: string, value: SlValue): void {
|
||||
const style = 칸[cell]?.서식;
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
import { currentLanguageIndex } from "@ui/ui_template_locale";
|
||||
import { quoteSheet } from "../../A00_Common/spreadsheet/spreadsheet_address";
|
||||
import { applyCommand } from "../../A00_Common/spreadsheet/spreadsheet_commands";
|
||||
import type { Cell, Sheet, Workbook } from "../../A00_Common/spreadsheet/spreadsheet_types";
|
||||
|
||||
/** 상자 종류 — 입력값 · 고정값 · 표 · 계산 · 항목 · 다른 로직 · 결과 */
|
||||
@@ -34,6 +35,8 @@ export interface SlNode {
|
||||
pillsOut: SlPill[];
|
||||
/** 표 키 · 요소 · 기존 로직 키 · 내 표 id */
|
||||
ref?: string;
|
||||
/** 바깥 값을 어디서 받나(서버 `/api/m01/sl/outside` 물음 모양) — 없으면 kind · ref 로 짐작 */
|
||||
source?: SlSource;
|
||||
/** 띠 단계 — 0 입력값 · 1 표 · 2 계산 · 3 항목 · 4 결과 */
|
||||
stage: number;
|
||||
}
|
||||
@@ -45,14 +48,33 @@ export interface SlEdge {
|
||||
to: { node: string; pill: string };
|
||||
}
|
||||
|
||||
/** 바깥 값 칸 */
|
||||
/** 돈 로직 결과에서 꺼낼 칸 */
|
||||
export type SlField = "노무비" | "재료비" | "경비" | "계";
|
||||
|
||||
/** 바깥 값 출처 — 서버 `/api/m01/sl/outside` 의 물음 한 줄(id 뺌) 그대로
|
||||
* · 표 = 품셈 표 통째(머리 줄 + 줄 · 범위 칸은 「이름·아래」 「이름·위」 두 칸 — `rangeHead`)
|
||||
* · 노임 · 자재 = 그 줄 단가 · 장비 = 기계경비 로직 부름 글 + 넘길 값 · 로직 = 기존 로직 결과 */
|
||||
export type SlSource =
|
||||
| { kind: "표"; table: string }
|
||||
| { kind: "노임" | "자재"; key: string }
|
||||
| { kind: "장비"; element: string; inputs: Record<string, unknown>; field?: SlField }
|
||||
| { kind: "로직"; key: string; inputs: Record<string, unknown>; field?: SlField };
|
||||
|
||||
/** 바깥 값 칸 — id = 「상자 id!칸」(서버 답 id 와 짝) */
|
||||
export interface SlOutside {
|
||||
id: string;
|
||||
node: string;
|
||||
cell: string;
|
||||
kind: "table" | "price" | "logic";
|
||||
ref: string;
|
||||
source?: SlSource;
|
||||
}
|
||||
|
||||
/** 표 범위 조건 · 범위 값 — 한 칸을 두 칸(아래 · 위)으로 펼친 머리 이름 */
|
||||
export const RANGE_LOW = "·아래";
|
||||
export const RANGE_HIGH = "·위";
|
||||
export const rangeHead = (name: string): [string, string] => [name + RANGE_LOW, name + RANGE_HIGH];
|
||||
|
||||
/** 내 표 — 품셈에 없는 표(사용자가 만듦) */
|
||||
export interface SlUserTable {
|
||||
id: string;
|
||||
@@ -100,6 +122,13 @@ const TEXT = {
|
||||
Self: ["같은 상자끼리는 못 이음", "A box can't link to itself"],
|
||||
Loop: ["이으면 돌고 도는 선이 됨", "This line would make a loop"],
|
||||
Twice: ["이미 이어져 있음", "Already linked"],
|
||||
NameLen: ["이름은 1~31 글자", "Name must be 1-31 characters"],
|
||||
NameChar: [
|
||||
"이름에 못 쓰는 글자 — [ ] : * ? / \\ 와 앞뒤 '",
|
||||
"Name can't use [ ] : * ? / \\ or a leading/trailing '",
|
||||
],
|
||||
NameSame: ["같은 이름 상자가 있음", "A box with this name exists"],
|
||||
NoBox: ["상자가 없음", "No such box"],
|
||||
} as const;
|
||||
|
||||
const tl = (key: keyof typeof TEXT): string => TEXT[key][currentLanguageIndex === 1 ? 1 : 0];
|
||||
@@ -130,12 +159,25 @@ function nextId(prefix: string, ids: string[]): string {
|
||||
return `${prefix}${Math.max(0, ...nums) + 1}`;
|
||||
}
|
||||
|
||||
/** 시트 이름 — 통합문서 안에서 하나(같으면 「 (2)」) */
|
||||
function uniqueName(book: Workbook, name: string): string {
|
||||
const base = name.trim() || "상자";
|
||||
const used = new Set(book.시트.map((s) => s.이름));
|
||||
if (!used.has(base)) return base;
|
||||
for (let n = 2; ; n++) if (!used.has(`${base} (${n})`)) return `${base} (${n})`;
|
||||
/** 시트 이름 규칙(엔진 `checkSheetName`) — 1~31 글자 · [ ] : * ? / \\ 와 앞뒤 ' 못 씀 */
|
||||
const BAD_CHARS = /[[\]:*?/\\]/g;
|
||||
const cleanName = (name: string): string =>
|
||||
name
|
||||
.replace(BAD_CHARS, "·")
|
||||
.replace(/^'+|'+$/g, "")
|
||||
.trim()
|
||||
.slice(0, 31) || "상자";
|
||||
|
||||
/** 시트 이름 — 통합문서 안에서 하나(같으면 「 (2)」 · 대소문자 안 가림) */
|
||||
function uniqueName(book: Workbook, name: string, self?: string): string {
|
||||
const base = cleanName(name);
|
||||
const used = new Set(book.시트.filter((s) => s.id !== self).map((s) => s.이름.toLowerCase()));
|
||||
if (!used.has(base.toLowerCase())) return base;
|
||||
for (let n = 2; ; n++) {
|
||||
const tail = ` (${n})`;
|
||||
const next = base.slice(0, 31 - tail.length) + tail;
|
||||
if (!used.has(next.toLowerCase())) return next;
|
||||
}
|
||||
}
|
||||
|
||||
const sheetOf = (sl: SheetLogic, node: SlNode): Sheet => {
|
||||
@@ -435,3 +477,22 @@ export function nodeSheet(sl: SheetLogic, nodeId: string): Sheet | null {
|
||||
const node = nodeOf(sl, nodeId);
|
||||
return node ? sheetOf(sl, node) : null;
|
||||
}
|
||||
|
||||
/** 상자 이름 바꾸기 — 시트 이름도(엔진 시트 이름 명령) · 그 시트를 가리키던 선 수식 · 다른 수식도 따라감 */
|
||||
export function renameNode(
|
||||
sl: SheetLogic,
|
||||
nodeId: string,
|
||||
name: string,
|
||||
): { sl: SheetLogic; why: string | null } {
|
||||
const node = nodeOf(sl, nodeId);
|
||||
if (!node) return { sl, why: tl("NoBox") };
|
||||
const next = name.trim();
|
||||
if (!next || next.length > 31) return { sl, why: tl("NameLen") };
|
||||
if (/[[\]:*?/\\]/.test(next) || next.startsWith("'") || next.endsWith("'"))
|
||||
return { sl, why: tl("NameChar") };
|
||||
if (uniqueName(sl.workbook, next, node.sheetId) !== next) return { sl, why: tl("NameSame") };
|
||||
const out = clone(sl);
|
||||
applyCommand(out.workbook, { 종류: "시트이름", 시트: node.sheetId, 이름: next });
|
||||
nodeOf(out, nodeId)!.name = next;
|
||||
return { sl: out, why: null };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user