feat(B09): 단가연결 즉시 금액(뒤에서 서버 셈) · [저장] 단추 · 옮길 때 물음(PLAN 702-3)
- 새 산근(로직 연결 · 입력값) = 연결 즉시 뒤에서 POST …/estimation/calc(화면 캐시로 셈 · 저장 안 함) · 차례 번호 · 옛 요청 끊기 · 오는 동안 그 줄 금액 칸만 「셈 중」 · 잠금 없음 (새 `B09_Estimation_UI_Live.ts`) · 기준 = payload.user(서버가 그 셈에 쓴 값) - 조작 중 셈 = 마지막 GET · [저장] 의 B08 원천 다시 씀(120 s · 0.6 → 0.15 s) · [저장] · GET 은 늘 새로 읽음 - [저장] 단추 = 왼쪽 아래 [초기화] 옆(B08 과 같은 부품 · 꼴) · 고침 없으면 알림만 - 단계 이동 · [>] = 저장 안 한 고침 있으면 공용 떠남 물음(취소 · 폐기 · 저장) — 옛 자동 저장 없앰 - 서고 사본 payload 의 user 받음(서버 기준) — 화면 캐시와 다른 로직 줄은 뒤에서 다시 셈 - 낡은 시험 고침: 481-2 · 298-2(자동 저장 → 물음 · [저장] 단추) · 184-3 · 304(단추 줄) · 204-2(user 받음) · 200-2(701-3 띠 · 702-1 펼침 뒤 셸 꼴) - 확인 = test_702_3 3 · B09 관련 312 통과 · typecheck 0 · 검증_sub5 T6 11 누름 · 연결 뒤 0.32~0.49 s 금액 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -14,7 +14,7 @@
|
||||
제목 줄 = 아래 항목 금액 합(엑셀 SUMIF — 빈 줄은 0 이 아니라 「빈 줄 n」 으로 셈)
|
||||
빈칸 = 수량이나 단가를 낸 원천이 없으면 그 줄 금액 None + 「연결 없음」 목록.
|
||||
단가 = `ref_price(ref)` 하나 — dg = 일위대가목록표 · sg = 단가산출근거목록표 ·
|
||||
logic = M01 로직 새 산근(서버가 `sg_hand{줄 키: 산근 키}` 로 줌 · 그전엔 빈칸 「[저장] 뒤」) ·
|
||||
logic = M01 로직 새 산근(서버가 `sg_hand{줄 키: 산근 키}` 로 줌 · 그전엔 빈칸 · 화면은 연결 즉시 뒤에서 서버 셈 702-3) ·
|
||||
그 밖 = `inputs.hopyo` · 비고 = 「대가n」 · 「산근 n호표」(155-3 꼴).
|
||||
자유 편집(184) = 기본 로직 표 = 초기값(저장 안 함) ·
|
||||
사용자 값 `user.bill` v2 = 덮개(계약 — 184-3 화면):
|
||||
@@ -149,7 +149,7 @@ def _material_items(inputs: dict[str, Any], compare: list[dict[str, Any]]) -> li
|
||||
|
||||
PriceOf = Callable[[str], dict[str, Any] | None]
|
||||
WHY_HAND = "수기 줄 — 연결 없음(우클릭 [연결])"
|
||||
WHY_LOGIC = "새 산근 — [저장] 뒤 단가(서버 셈)"
|
||||
WHY_LOGIC = "새 산근 — 서버 셈 뒤 단가(702-3 뒤에서 셈)"
|
||||
|
||||
|
||||
def ref_price(
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
* 항목 = 서버가 B08 수량으로 짠 줄 + 대비표 「별도」(4 자재대) · 「관급」(관급자재대) 자재 + 조달수수료
|
||||
* 금액 = ROUNDDOWN(단가 × 수량, 0) 비목마다 · 조달수수료 = ROUNDDOWN(순자재대 × 율 ÷ 100, 0)
|
||||
* 묶음 금액 = 자손 항목 합(빈 줄은 「빈 줄 n」) · 원가계산서로 넘길 합 = sums(아는 금액까지 + blank · 701-3 · 관급만 빈 줄 있으면 null)
|
||||
* 단가 = `refPrice(ref)` 하나(dg 대가 목록 · sg 산근 목록 · logic = 서버 sg_hand 산근 · 그 밖 hopyo)
|
||||
* 단가 = `refPrice(ref)` 하나(dg 대가 목록 · sg 산근 목록 · logic = 서버 sg_hand 산근(702-3 연결 즉시 뒤에서 셈) · 그 밖 hopyo)
|
||||
* 자유 편집(184) = `user.bill` v2 덮개 {v: 2, rows: [표 차례 · 고친 칸 · 새 줄], gone: [지운 자동 줄]} ·
|
||||
* 옛 꼴(qty · 손 줄 after · parent) 그대로 읽음 · 저장 꼴 · 출력 줄 칸(계약) = 짝 .py 머리
|
||||
* 출력 줄 칸 = kind · key · name · parent · level · no · cost · auto | hand · auto_{칸} · section
|
||||
@@ -112,7 +112,7 @@ function materialItems(inputs: Row, compare: Row[]): Row[] {
|
||||
/** 사본 주인 id → 대가 1 단위 단가(145-2 `daegaPrice`) */
|
||||
export type PriceOf = (owner: string) => Row | null;
|
||||
const WHY_HAND = "수기 줄 — 연결 없음(우클릭 [연결])";
|
||||
const WHY_LOGIC = "새 산근 — [저장] 뒤 단가(서버 셈)";
|
||||
const WHY_LOGIC = "새 산근 — 서버 셈 뒤 단가(702-3 뒤에서 셈)";
|
||||
|
||||
/** 연결 → 1 단위 단가 {no, key, labor, material, expense, sum} — 없으면 null */
|
||||
export function refPrice(ref: Row | null, books: Row, hopyo: Row, rowKey = ""): Row | null {
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
POST /api/projects/{id}/estimation/prices {user?}
|
||||
[단가 갱신] — 수요 단가 전부를 지금 M01 값으로 새로 굳힘 · user = 화면 캐시(296 ·
|
||||
지운 줄 · 그 사본은 수요에서 뺌 · 저장은 안 함) · M01 원본이 지워진 줄 = 굳힌 값 그대로
|
||||
POST /api/projects/{id}/estimation/calc {user}
|
||||
조작 중 서버 셈(702-3 · 단가연결 즉시 금액) — 화면 캐시로 GET 꼴 셈 · 저장 안 함
|
||||
POST /api/projects/{id}/estimation/logic-preview {code, given, rate, picks}
|
||||
「운반 단가」 모달 계산 절차(253-3) — 그 로직 한 산근을 프로젝트 단가로 풀어 글 줄 · 단가 ·
|
||||
저장 안 함(`render_sangeun_text` 를 `Sangeun.Solver` 로) · `blocks` = 칸 나눔(277-2)
|
||||
@@ -38,6 +40,7 @@ import asyncio
|
||||
import copy
|
||||
import importlib
|
||||
import logging
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from uuid import UUID
|
||||
@@ -80,6 +83,25 @@ except ImportError: # pragma: no cover
|
||||
router = APIRouter(prefix="/api/projects", tags=["B09 Estimation"])
|
||||
|
||||
|
||||
# 702-3 조작 중 셈 = 마지막 GET · [저장] 이 읽은 B08 원천 다시 씀(B08 은 B09 조작으로 안 바뀜)
|
||||
# ponytail: 시간 창만 봄 — 창 안에 남이 B08 을 고치면 미리보기만 옛 수량 · [저장] 은 늘 새로 읽음
|
||||
B08_REUSE_S = 120.0
|
||||
_b08_seen: dict[UUID, tuple[float, dict[str, Any], list[str]]] = {}
|
||||
|
||||
|
||||
async def _b08_for(project_id: UUID, notes: list[str], reuse: bool) -> dict[str, Any]:
|
||||
"""B08 원천 — reuse = 시간 창 안 마지막 값(사본) · 아니면 새로 읽고 남김."""
|
||||
seen = _b08_seen.get(project_id)
|
||||
if reuse and seen and time.monotonic() - seen[0] < B08_REUSE_S:
|
||||
notes.extend(seen[2])
|
||||
return copy.deepcopy(seen[1])
|
||||
mine: list[str] = []
|
||||
out = await _b08(project_id, mine)
|
||||
_b08_seen[project_id] = (time.monotonic(), copy.deepcopy(out), mine)
|
||||
notes.extend(mine)
|
||||
return out
|
||||
|
||||
|
||||
async def _b08(project_id: UUID, notes: list[str]) -> dict[str, Any]:
|
||||
"""B08 수량 원천 — 못 읽으면 빈칸 + 알림(B09 는 B08 를 고치지 않음)."""
|
||||
out: dict[str, Any] = {}
|
||||
@@ -170,10 +192,10 @@ async def _demand(root: Path, draft: dict[str, Any] | None = None) -> list[str]:
|
||||
|
||||
|
||||
async def payload(
|
||||
project_id: UUID, root: Path, draft: dict[str, Any] | None = None
|
||||
project_id: UUID, root: Path, draft: dict[str, Any] | None = None, reuse_b08: bool = False
|
||||
) -> dict[str, Any]:
|
||||
notes: list[str] = []
|
||||
b08 = await _b08(project_id, notes)
|
||||
b08 = await _b08_for(project_id, notes, reuse_b08)
|
||||
keys = await _demand(root, draft)
|
||||
if not keys:
|
||||
notes.append(
|
||||
@@ -182,7 +204,8 @@ async def payload(
|
||||
book = await asyncio.to_thread(store.read_book, root)
|
||||
inputs = await asyncio.to_thread(prices.build_inputs, book, keys, b08["materials"])
|
||||
hauls = await asyncio.to_thread(haul_routes, root)
|
||||
user = await asyncio.to_thread(store.read_user, root)
|
||||
# 702-3 draft = 화면 캐시로 셈(저장 안 함) · 없으면 저장 값
|
||||
user = await asyncio.to_thread(store.read_user, root) if draft is None else draft
|
||||
calc = await asyncio.to_thread(library.resolve_hauls, root, {**user, **haul_user(hauls)})
|
||||
inputs["bill"] = bill_items({**b08, **calc, "haul_routes": hauls})
|
||||
inputs["hopyo"] = hopyo_prices(root)
|
||||
@@ -276,6 +299,14 @@ async def refresh_prices(
|
||||
return await payload(project_id, root, draft)
|
||||
|
||||
|
||||
@router.post("/{project_id}/estimation/calc")
|
||||
async def calc_estimation(project_id: UUID, body: dict[str, Any] = Body(...)) -> dict[str, Any]:
|
||||
"""702-3 조작 중 서버 셈 — 화면 캐시(user)로 GET 꼴 셈 · 저장 안 함 · 단가 = 굳힌 책
|
||||
(없는 줄 = 지금 M01 · [저장] 때 책에 굳힘 → 같은 값)."""
|
||||
root = await _project_root(project_id)
|
||||
return await payload(project_id, root, store.clean_user(body.get("user")), reuse_b08=True)
|
||||
|
||||
|
||||
class PreviewBody(BaseModel):
|
||||
code: str = Field(min_length=1, max_length=40)
|
||||
given: dict[str, Any] = Field(default_factory=dict)
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
/* =============================================================================
|
||||
* B09_Estimation_UI_Live.ts
|
||||
* B09 조작 중 서버 셈(PLAN 702-3) — 새 산근(단가연결 · 로직 입력값)은 서버 M01 엔진 몫 →
|
||||
* 바뀌면 그 자리에서 뒤로 POST …/estimation/calc {user}(저장 안 함 · GET 꼴 답) ·
|
||||
* 차례 번호 · 옛 요청 끊기(`design_pipeline` 꼴) · 오는 동안 그 줄 금액 칸만 「셈 중」 · 화면 잠금 없음
|
||||
*
|
||||
* 기준 = payload.user(서버가 그 셈에 쓴 사용자 값) — 로직 연결 지문이 다른 줄 = 기다림 ·
|
||||
* 기다리는 줄은 옛 산근 값을 안 씀(sg_hand 에서 뺌) · 못 받으면 알림 뒤 빈칸(같은 지문 다시 안 보냄)
|
||||
* ========================================================================== */
|
||||
|
||||
import { API_BASE_URL } from "@config/config_frontend";
|
||||
import { showToast } from "@ui/ui_template_elements";
|
||||
import type { Row } from "./B09_Estimation_Calc_Base";
|
||||
|
||||
export const WAITING = "셈 중";
|
||||
|
||||
/** 줄 키 → 로직 연결 지문(코드 · 입력값 · 켜쌓기율 · 고름) */
|
||||
function logicSig(user: Row | null | undefined): Map<string, string> {
|
||||
const out = new Map<string, string>();
|
||||
for (const r of user?.bill?.rows || []) {
|
||||
const ref = r.ref;
|
||||
if (ref?.tab === "logic")
|
||||
out.set(
|
||||
r.key,
|
||||
JSON.stringify([ref.code, ref.given ?? {}, ref.rate ?? null, ref.picks ?? null]),
|
||||
);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 서버 셈을 기다리는 줄 — 로직 연결이 서버 기준(payload.user)과 다름 */
|
||||
export function waitingKeys(payload: Row, user: Row): Set<string> {
|
||||
const known = logicSig(payload.user);
|
||||
const out = new Set<string>();
|
||||
for (const [key, sig] of logicSig(user)) if (known.get(key) !== sig) out.add(key);
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 셈 입력 — 기다리는 줄은 옛 산근 단가를 안 씀 */
|
||||
export function withoutWaiting(payload: Row, wait: Set<string>): Row {
|
||||
if (!wait.size) return payload;
|
||||
const hand = { ...(payload.sheets?.sg_hand || {}) };
|
||||
for (const key of wait) delete hand[key];
|
||||
return { ...payload, sheets: { ...payload.sheets, sg_hand: hand } };
|
||||
}
|
||||
|
||||
/** 기다리는 줄 금액 칸 = 「셈 중」(셈이 끝난 뒤 · 계에는 안 듦) */
|
||||
export function markWaiting(sheets: Row, wait: Set<string>): void {
|
||||
for (const r of sheets.bill?.rows || []) if (wait.has(r.key) && r.sum == null) r.sum = WAITING;
|
||||
}
|
||||
|
||||
/** settle(next, sent) — next = 서버 답(못 받음 = null) · sent = 보낸 사용자 값 */
|
||||
export function createLiveCalc(
|
||||
projectId: string,
|
||||
settle: (next: Row | null, sent: Row) => void,
|
||||
): { busy: () => boolean; kick: (wait: Set<string>, user: Row) => void } {
|
||||
let seq = 0;
|
||||
let ctrl: AbortController | null = null;
|
||||
let sentSig = "";
|
||||
let failedSig = "";
|
||||
return {
|
||||
busy: () => ctrl !== null,
|
||||
/** 기다리는 줄 있으면 서버 셈(같은 것이 가는 중 · 못 받은 것이면 그대로) · 없으면 가던 것 끊음 */
|
||||
kick(wait, user) {
|
||||
const sig = JSON.stringify([...logicSig(user)]);
|
||||
if (wait.size && (sig === failedSig || (ctrl && sig === sentSig))) return;
|
||||
ctrl?.abort();
|
||||
ctrl = null;
|
||||
seq += 1;
|
||||
if (!wait.size) return;
|
||||
const mine = seq;
|
||||
const me = new AbortController();
|
||||
ctrl = me;
|
||||
sentSig = sig;
|
||||
fetch(`${API_BASE_URL}/projects/${projectId}/estimation/calc`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ user }),
|
||||
signal: me.signal,
|
||||
})
|
||||
.then(async (response) => {
|
||||
const body = (await response.json().catch(() => null)) as Row | null;
|
||||
if (!response.ok || !body) {
|
||||
const detail = body?.detail;
|
||||
throw new Error(typeof detail === "string" ? detail : `HTTP ${response.status}`);
|
||||
}
|
||||
return body;
|
||||
})
|
||||
.then(
|
||||
(next) => {
|
||||
if (mine !== seq) return;
|
||||
ctrl = null;
|
||||
settle(next, user);
|
||||
},
|
||||
(error: unknown) => {
|
||||
if (mine !== seq || me.signal.aborted) return;
|
||||
ctrl = null;
|
||||
failedSig = sig;
|
||||
showToast(`새 산근 단가를 못 받음 — [저장] 때 다시 셈 · ${String(error)}`, "warning");
|
||||
settle(null, user);
|
||||
},
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -4,8 +4,9 @@
|
||||
*
|
||||
* 원본 = 기번3 공통 원가산출 엑셀 · 탭 하나 = 화면 표 하나(`…_UI_Tabs` 차례) · 옛 원가 코드 안 봄
|
||||
* 자료 = GET …/estimation(입력 · 사용자 값 · 서버가 셈한 표) · 조작 = 캐시(`b09-estimation`) +
|
||||
* 짝 계산(`…_Calc_*.ts`)으로 바로 다시 셈 · 저장(PUT → 서버 정본) = 단계 이동 · [>] 때(481-2 · [저장] 단추 없음 · 요구 301)
|
||||
* [>] = 고친 값 저장 → POST …/estimation/complete(단계 6 완료) → `DRAFT.next`(인쇄 미리보기 · 초안) · 실패면 머묾
|
||||
* 짝 계산(`…_Calc_*.ts`)으로 바로 다시 셈 · 새 산근(로직 연결) = 뒤에서 서버 셈(702-3 · `…_UI_Live`) ·
|
||||
* [저장](702-3 · 왼쪽 아래 [초기화] 옆) = PUT → 서버 정본으로 다시 셈 · 저장 안 하고 단계 옮김 · [>] = 물음(저장 · 버림 · 머묾)
|
||||
* [>] = 물음 → POST …/estimation/complete(단계 6 완료) → `DRAFT.next`(인쇄 미리보기 · 초안) · 실패면 머묾
|
||||
* [초기화](요구 298-2) = 확인 창 → POST …/estimation/reset(사용자 값 · 단가 책 지움 · 견적서 남김) → 셈 값 · [업데이트] = POST …/prices
|
||||
* 호표 칸 누름 = 그 탭 그 줄로 이동 · 자동 저장 없음
|
||||
* ========================================================================== */
|
||||
@@ -36,6 +37,7 @@ import { computeLabor, daegaPrice } from "./B09_Estimation_Calc_Labor";
|
||||
import { computeTotal } from "./B09_Estimation_Calc_Total";
|
||||
import { createTabBars } from "./B09_Estimation_UI_TabBar";
|
||||
import { createCheckPanel } from "./B09_Estimation_UI_Check";
|
||||
import { createLiveCalc, markWaiting, waitingKeys, withoutWaiting } from "./B09_Estimation_UI_Live";
|
||||
import { tableOptionsOf, tabTableOf } from "./B09_Estimation_UI_Builders";
|
||||
import { createDock, linkMode } from "./B09_Estimation_UI_Dock";
|
||||
import { closePopups, openRowPopup } from "./B09_Estimation_UI_Popup";
|
||||
@@ -56,7 +58,11 @@ import {
|
||||
renderGroupPanel,
|
||||
unpickOnRelease,
|
||||
} from "./B09_Estimation_UI_Tabs_Groups";
|
||||
import { currentProjectId, guardProjectLeave } from "../A00_Common/b_project_switch";
|
||||
import {
|
||||
askProjectLeave,
|
||||
currentProjectId,
|
||||
guardProjectLeave,
|
||||
} from "../A00_Common/b_project_switch";
|
||||
|
||||
// 252 초안 탭(공기 등) — draft 폴더가 있으면 등록 · 지우면 빈 목록(오류 없음)
|
||||
import.meta.glob("./draft/*_UI.ts", { eager: true });
|
||||
@@ -139,22 +145,37 @@ export function renderB09Estimation(root: HTMLElement): void {
|
||||
body.className = "b09-estimation";
|
||||
body.append(bars.top, bars.sub, panel);
|
||||
|
||||
// 702-3 새 산근 = 뒤에서 서버 셈 → 받은 답을 같은 자료 묶음에 끼움(열린 창이 쥔 payload 그대로)
|
||||
const live = projectId
|
||||
? createLiveCalc(projectId, (next, sent) => {
|
||||
if (next && payload) Object.assign(payload, next, { user: sent });
|
||||
redraw();
|
||||
})
|
||||
: null;
|
||||
|
||||
const recompute = (): void => {
|
||||
if (!payload) return;
|
||||
sheets = computeSheets(payload, user);
|
||||
const wait = waitingKeys(payload, user);
|
||||
live?.kick(wait, user);
|
||||
sheets = computeSheets(withoutWaiting(payload, wait), user);
|
||||
if (live?.busy()) markWaiting(sheets, wait);
|
||||
check.draw(payload, sheets); // 203-1 좌측 연결 점검 · [업데이트] 배지
|
||||
};
|
||||
|
||||
const setUser = (next: Row): void => {
|
||||
user = next;
|
||||
if (projectId) writeState("b09-estimation", user, projectId);
|
||||
redraw();
|
||||
};
|
||||
|
||||
function redraw(): void {
|
||||
recompute();
|
||||
// 고친 뒤 다시 그려도 보던 자리 지킴(166-2 내역서 수량 · 손 줄)
|
||||
const top = panel.querySelector<HTMLElement>(".ui-data-table")?.scrollTop ?? 0;
|
||||
show(current);
|
||||
const box = panel.querySelector<HTMLElement>(".ui-data-table");
|
||||
if (box && top) box.scrollTop = top;
|
||||
};
|
||||
}
|
||||
|
||||
/** 탭 하나 그림 · focus = 이동해 온 줄 키 */
|
||||
const show = (tab: TabId, focus?: string): void => {
|
||||
@@ -342,11 +363,22 @@ export function renderB09Estimation(root: HTMLElement): void {
|
||||
}),
|
||||
});
|
||||
reset.dataset.b09Reset = "";
|
||||
// 702-3 [저장] — 화면 캐시 → 서버 정본으로 다시 셈(고침 없으면 요청 없이 알림)
|
||||
const save = createButton({
|
||||
label: L("Common_Btn_Save"),
|
||||
variant: "filled",
|
||||
onClick: () => {
|
||||
if (!projectId || readState("b09-estimation", projectId) === null)
|
||||
return void showToast("저장할 고침이 없습니다.", "info");
|
||||
void saveChanged();
|
||||
},
|
||||
});
|
||||
save.dataset.b09Save = "";
|
||||
// 카드 = 좌측 패널 공용 카드(269 · 「구조물 배치」 양식)
|
||||
const section = createPanelCard("데이터 점검", true); // 311 기본 접힘
|
||||
section.append(refresh, check.list);
|
||||
// 303-1 · 304-1 하단 고정 줄 — [박스보기|호표보기] · [하이퍼링크|팝업] · 초안 단추 · [?][<][>][초기화]
|
||||
const actions = createDock([reset], () => isGroupTab(current) && show(current));
|
||||
const actions = createDock([save, reset], () => isGroupTab(current) && show(current));
|
||||
const left = document.createElement("div");
|
||||
left.className = "ui-sidebar-panel b09-options";
|
||||
left.append(section, ...DRAFT.panels.map((make) => make(() => user, setUser)), actions);
|
||||
@@ -359,15 +391,15 @@ export function renderB09Estimation(root: HTMLElement): void {
|
||||
mainContent: body,
|
||||
leftPanel: left,
|
||||
routes: WORKFLOW_STEP_ROUTES,
|
||||
// 481-2 단계 이동 = 고친 값 저장 성공 뒤 · 실패면 머묾 · 불러오기 전 · 고침 없음 = 요청 없이
|
||||
// 702-3 단계 이동 = 저장 안 한 고침 있으면 물음(저장 · 버림 · 머묾) 뒤 · 고침 없음 = 바로
|
||||
onStepClick: (stepIndex: number) => {
|
||||
if (!projectId) return;
|
||||
void moveStage(projectId, WORKFLOW_STEP_ROUTES[stepIndex], [
|
||||
{ name: "원가 저장", run: saveChanged },
|
||||
]);
|
||||
void askProjectLeave().then((go) => {
|
||||
if (go) void moveStage(projectId, WORKFLOW_STEP_ROUTES[stepIndex], []);
|
||||
});
|
||||
},
|
||||
});
|
||||
// [>] = 다음 단계 없음 → 공용 틀 대신 여기서 — 저장 → 단계 6 완료 → 초안 다음 길 · 초안 없으면 흐림 그대로
|
||||
// [>] = 다음 단계 없음 → 공용 틀 대신 여기서 — 물음(702-3) → 단계 6 완료 → 초안 다음 길 · 초안 없으면 흐림 그대로
|
||||
const next = left.querySelector<HTMLButtonElement>('[data-step-nav="1"]');
|
||||
const goNext = DRAFT.next;
|
||||
if (next && goNext) next.disabled = false;
|
||||
@@ -376,14 +408,11 @@ export function renderB09Estimation(root: HTMLElement): void {
|
||||
event.stopPropagation();
|
||||
const why = DRAFT.blocked?.();
|
||||
if (why) return void showToast(why, "warning"); // 701-3 확정 = 빈 줄 0 일 때만
|
||||
void moveStage(projectId, goNext, [
|
||||
{ name: "원가 저장", run: saveChanged },
|
||||
{
|
||||
name: "원가 계산 단계 완료",
|
||||
run: async () =>
|
||||
void (await request(`/projects/${projectId}/estimation/complete`, { method: "POST" })),
|
||||
},
|
||||
]);
|
||||
const complete = async (): Promise<void> =>
|
||||
void (await request(`/projects/${projectId}/estimation/complete`, { method: "POST" }));
|
||||
void askProjectLeave().then((go) => {
|
||||
if (go) void moveStage(projectId, goNext, [{ name: "원가 계산 단계 완료", run: complete }]);
|
||||
});
|
||||
});
|
||||
root.append(layout.root);
|
||||
show(current);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* B09_Estimation_UI_Tabs_Bill_Link.ts
|
||||
* B09 설계내역서 연결 고르기(166-2 · 184-3) — 있는 호표(일위대가목록표 dg · 단가산출근거목록표 sg) ·
|
||||
* 서고(마스터 · 회사 · 개인) 구조물조합 · 일위대가 사전조합(204-2 — 서버가 프로젝트 사본으로 들여와 새
|
||||
* 대가 호표 · 같은 것 = 있는 사본) · M01 로직(새 산근 = 서버 셈 · [저장] 뒤 단가) · 로직 입력값 고치기 ·
|
||||
* 대가 호표 · 같은 것 = 있는 사본) · M01 로직(새 산근 = 서버 셈 · 연결 즉시 뒤에서 · 702-3) · 로직 입력값 고치기 ·
|
||||
* 연결 풀기.
|
||||
* 251 단가연결 = 한 모달(`openPriceLink` · 종류 고르기 + 검색) — B09 설계내역서 · B08 거리이정표 같이 씀 ·
|
||||
* 단가산출 = 옵션 값 창(`askInputs` · 160)
|
||||
@@ -35,7 +35,7 @@ const LAYER_TEXT: Record<PickerLayer, string> = {
|
||||
personal: "개인",
|
||||
};
|
||||
|
||||
/** M01 로직 → 입력값 창 → 연결(새 산근 · 단가는 [저장] 뒤 서버 셈) */
|
||||
/** M01 로직 → 입력값 창 → 연결(새 산근 · 단가 = 화면이 뒤에서 서버 셈 · 702-3 `…_UI_Live`) */
|
||||
export function askInputs(apply: ApplyRef, code: string, name: string, old?: Row): void {
|
||||
fetchLogicInputs([code]).then(
|
||||
(got) => {
|
||||
@@ -48,14 +48,14 @@ export function askInputs(apply: ApplyRef, code: string, name: string, old?: Row
|
||||
rate: old?.rate ?? null,
|
||||
readOnly: false,
|
||||
okLabel: "연결",
|
||||
notice: "새 산근 = 단가산출근거 뒤 번호 · 단가는 [저장] 뒤 서버가 셈",
|
||||
notice: "새 산근 = 단가산출근거 뒤 번호 · 단가는 연결 뒤 서버가 바로 셈",
|
||||
done: (next) => {
|
||||
const unit = String(logic?.결과단위 || "")
|
||||
.replace(/^원\//, "")
|
||||
.replace(/^[\d.]+/, "");
|
||||
const ref = { tab: "logic", code, name, given: next.입력값, rate: next.켜쌓기율 };
|
||||
apply({ ...ref, picks: old?.picks ?? null }, { name, unit });
|
||||
showToast("연결함 — 새 산근 단가는 [저장] 뒤에 채워짐", "success");
|
||||
showToast("연결함 — 새 산근 단가 셈 중", "success");
|
||||
},
|
||||
});
|
||||
},
|
||||
@@ -130,7 +130,8 @@ export async function importLib(
|
||||
try {
|
||||
const got = await postLib("library", pick);
|
||||
if (!got.key) throw new Error("사본은 들여왔으나 호표를 못 만듦");
|
||||
const { user: _user, ...rest } = got.payload as Row;
|
||||
// 702-3 user = 서버가 이 셈에 쓴 값(저장 값) — 화면 캐시와 다른 로직 줄은 뒤에서 다시 셈
|
||||
const rest = got.payload as Row;
|
||||
o.onPayload?.(rest);
|
||||
const from = ((rest.sheets?.daega as Row[]) || []).find((r) => r.key === got.key);
|
||||
const hand = lib.haul ? { given: lib.haul.given, own: lib.haul.own } : {};
|
||||
|
||||
@@ -86,6 +86,8 @@ const buttons = []; // 만든 단추 — { variant, node }
|
||||
let layoutOpts = null;
|
||||
let confirmAnswer = false;
|
||||
const store = new Map(); // 가짜 세션 캐시
|
||||
let leaveGuard = null;
|
||||
let leaveAnswer = "save"; // save | discard | stay
|
||||
const common = {
|
||||
"../A00_Common/b_page_tabs": {
|
||||
// 탭 줄 부품 흉내 — 칸마다 dataset[data] = 열쇠 · 지금 칸 누름은 안 부름
|
||||
@@ -140,8 +142,18 @@ const common = {
|
||||
return true;
|
||||
},
|
||||
},
|
||||
// 572-2 지금 프로젝트 = 탭마다 · 떠남 가드는 test_560_4
|
||||
"../A00_Common/b_project_switch": { currentProjectId: () => "P1", guardProjectLeave() {} },
|
||||
// 572-2 지금 프로젝트 = 탭마다 · 떠남 가드는 test_560_4 · 702-3 B09 단계 이동 물음 = 가드 흉내(leaveAnswer)
|
||||
"../A00_Common/b_project_switch": {
|
||||
currentProjectId: () => "P1",
|
||||
guardProjectLeave: (_root, g) => (leaveGuard = g),
|
||||
askProjectLeave: async () => {
|
||||
if (!leaveGuard || !leaveGuard.dirty()) return true;
|
||||
calls.push("ask");
|
||||
if (leaveAnswer === "stay") return false;
|
||||
if (leaveAnswer === "discard") return (await leaveGuard.discard?.(), true);
|
||||
return (await leaveGuard.save()) !== false;
|
||||
},
|
||||
},
|
||||
"../A00_Common/b_page_state": {
|
||||
readState: (name) => store.get(name) ?? null,
|
||||
writeState: (name, v) => store.set(name, v),
|
||||
@@ -295,8 +307,8 @@ async function b09(withNext) {
|
||||
unfinished = "빈 줄 2 · 미완성";
|
||||
out.blocked = await nav(1);
|
||||
unfinished = null;
|
||||
// [초기화]
|
||||
const reset = dock.children[0];
|
||||
// [초기화](702-3 [저장] 다음)
|
||||
const reset = dock.children[1];
|
||||
const press = async () => {
|
||||
take();
|
||||
reset.onClick();
|
||||
|
||||
@@ -80,6 +80,8 @@ const flush = () => new Promise((r) => setTimeout(r, 0));
|
||||
const calls = [];
|
||||
let layoutOpts = null;
|
||||
const store = new Map(); // 가짜 세션 캐시 — 이름 → 값
|
||||
let leaveGuard = null;
|
||||
let leaveAnswer = "save"; // save | discard | stay
|
||||
const common = {
|
||||
"../A00_Common/b_page_tabs": {
|
||||
// 탭 줄 부품 흉내 — 칸마다 dataset[data] = 열쇠 · 지금 칸 누름은 안 부름
|
||||
@@ -129,7 +131,18 @@ const common = {
|
||||
},
|
||||
},
|
||||
// 572-2 지금 프로젝트 = 탭마다 · 떠남 가드는 test_560_4
|
||||
"../A00_Common/b_project_switch": { currentProjectId: () => "P1", guardProjectLeave() {} },
|
||||
// 572-2 지금 프로젝트 = 탭마다 · 떠남 가드는 test_560_4 · 702-3 B09 단계 이동 물음 = 가드 흉내(leaveAnswer)
|
||||
"../A00_Common/b_project_switch": {
|
||||
currentProjectId: () => "P1",
|
||||
guardProjectLeave: (_root, g) => (leaveGuard = g),
|
||||
askProjectLeave: async () => {
|
||||
if (!leaveGuard || !leaveGuard.dirty()) return true;
|
||||
calls.push("ask");
|
||||
if (leaveAnswer === "stay") return false;
|
||||
if (leaveAnswer === "discard") return (await leaveGuard.discard?.(), true);
|
||||
return (await leaveGuard.save()) !== false;
|
||||
},
|
||||
},
|
||||
"../A00_Common/b_page_state": {
|
||||
readState: (name) => store.get(name) ?? null,
|
||||
writeState: (name, v) => store.set(name, v),
|
||||
@@ -223,6 +236,15 @@ async function b09() {
|
||||
out.fail = await step(5);
|
||||
fail = false;
|
||||
out.ok = await step(5);
|
||||
// 702-3 물음 [버림] = 캐시 지우고 이동 · [머묾] = 요청 없이 머묾
|
||||
store.set("b09-estimation", { x: 1 });
|
||||
leaveAnswer = "discard";
|
||||
out.discard = [await step(5), store.has("b09-estimation")];
|
||||
store.set("b09-estimation", { x: 1 });
|
||||
leaveAnswer = "stay";
|
||||
out.stay = [await step(5), store.has("b09-estimation")];
|
||||
leaveAnswer = "save";
|
||||
store.delete("b09-estimation");
|
||||
return out;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
/* 702-3 B09 조작 중 서버 셈 — 진짜 `B09_Estimation_UI_Live.ts` 를 트랜스파일해 가짜 fetch 위에서 돌림 ·
|
||||
* 차례 번호 · 옛 요청 끊기 · 같은 지문 다시 안 보냄 · 기다리는 줄(옛 산근 안 씀 · 「셈 중」) · 못 받음 = 한 번만.
|
||||
*/
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
|
||||
const ROOT = path.join(__dirname, "..", "..");
|
||||
|
||||
const toasts = [];
|
||||
function load(rel, stubs) {
|
||||
const src = fs.readFileSync(path.join(ROOT, rel), "utf8");
|
||||
const js = ts.transpileModule(src, {
|
||||
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
|
||||
}).outputText;
|
||||
const mod = { exports: {} };
|
||||
new Function("exports", "module", "require", js)(mod.exports, mod, (id) => stubs[id] ?? {});
|
||||
return mod.exports;
|
||||
}
|
||||
const live = load("B09_Estimation/B09_Estimation_UI_Live.ts", {
|
||||
"@config/config_frontend": { API_BASE_URL: "/api" },
|
||||
"@ui/ui_template_elements": { showToast: (text, kind) => toasts.push(kind) },
|
||||
});
|
||||
|
||||
// 가짜 fetch — 보낸 차례대로 쥐고 있다가 손으로 답함 · 끊김 = AbortError
|
||||
const pending = [];
|
||||
global.fetch = (url, init) =>
|
||||
new Promise((resolve, reject) => {
|
||||
const one = { url, body: JSON.parse(init.body), resolve, reject, aborted: false };
|
||||
init.signal.addEventListener("abort", () => {
|
||||
one.aborted = true;
|
||||
reject(Object.assign(new Error("aborted"), { name: "AbortError" }));
|
||||
});
|
||||
pending.push(one);
|
||||
});
|
||||
const answer = (one, ok = true) =>
|
||||
one.resolve({
|
||||
ok,
|
||||
status: ok ? 200 : 500,
|
||||
json: async () => (ok ? { tag: one.body.user.tag } : {}),
|
||||
});
|
||||
const flush = async () => {
|
||||
for (let k = 0; k < 8; k++) await new Promise((r) => setTimeout(r, 0));
|
||||
};
|
||||
|
||||
const logic = (given) => ({
|
||||
bill: { rows: [{ key: "hand:a", ref: { tab: "logic", code: "GF1", given } }, { key: "b" }] },
|
||||
});
|
||||
|
||||
(async () => {
|
||||
const out = {};
|
||||
const settled = [];
|
||||
const calc = live.createLiveCalc("P1", (next, sent) =>
|
||||
settled.push([next && next.tag, sent.tag]),
|
||||
);
|
||||
const base = { user: logic({ 높이: 3 }), sheets: { sg_hand: { "hand:a": "SG:old" } } };
|
||||
// 기다리는 줄 = 서버 기준과 다른 로직 연결만
|
||||
const u1 = { ...logic({ 높이: 4 }), tag: 1 };
|
||||
const wait = live.waitingKeys(base, u1);
|
||||
out.wait = [...wait];
|
||||
out.same = [...live.waitingKeys(base, base.user)];
|
||||
out.strip = live.withoutWaiting(base, wait).sheets.sg_hand;
|
||||
const sheets = {
|
||||
bill: {
|
||||
rows: [
|
||||
{ key: "hand:a", sum: null },
|
||||
{ key: "b", sum: null },
|
||||
],
|
||||
},
|
||||
};
|
||||
live.markWaiting(sheets, wait);
|
||||
out.mark = sheets.bill.rows.map((r) => r.sum);
|
||||
// 차례 — 첫 요청 가는 중 같은 지문 = 안 보냄 · 바뀐 지문 = 옛 것 끊고 새로
|
||||
calc.kick(wait, u1);
|
||||
calc.kick(wait, u1);
|
||||
out.sentAfterSame = pending.length;
|
||||
const u2 = { ...logic({ 높이: 5 }), tag: 2 };
|
||||
calc.kick(live.waitingKeys(base, u2), u2);
|
||||
await flush();
|
||||
out.firstAborted = pending[0].aborted;
|
||||
out.url = pending[1].url;
|
||||
answer(pending[1]);
|
||||
await flush();
|
||||
out.settled = settled.slice();
|
||||
out.busyAfter = calc.busy();
|
||||
// 기다릴 줄 없음 = 가던 것 끊음
|
||||
const u3 = { ...logic({ 높이: 6 }), tag: 3 };
|
||||
calc.kick(live.waitingKeys(base, u3), u3);
|
||||
calc.kick(new Set(), base.user);
|
||||
await flush();
|
||||
out.cancelled = [pending[2].aborted, calc.busy()];
|
||||
// 못 받음 = 알림 한 번 · 같은 지문 다시 안 보냄
|
||||
const u4 = { ...logic({ 높이: 7 }), tag: 4 };
|
||||
calc.kick(live.waitingKeys(base, u4), u4);
|
||||
answer(pending[3], false);
|
||||
await flush();
|
||||
calc.kick(live.waitingKeys(base, u4), u4);
|
||||
out.failed = [toasts, settled[settled.length - 1], pending.length];
|
||||
console.log(JSON.stringify(out));
|
||||
})().catch((e) => {
|
||||
console.error(e && e.stack ? e.stack : e);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -286,4 +286,4 @@ def test_초기화_배선():
|
||||
assert 'variant: "danger-ghost"' in block and "showConfirmDialog(" in block
|
||||
assert 'if (ok) void run("/estimation/reset", "POST"' in block
|
||||
assert "내역 초기화" not in src and "b09BillReset" not in src
|
||||
assert "createDock([reset]," in src # 303-1 하단 고정 줄 맨 아래 줄 · [저장] 없음(요구 301)
|
||||
assert "createDock([save, reset]," in src # 303-1 하단 고정 줄 맨 아래 줄 · 702-3 [저장] 생김
|
||||
|
||||
@@ -202,7 +202,8 @@ def test_셸_배선():
|
||||
) # 239-1 · 242-1 이동 줄 빨강 곁(고름과 한때 하나)
|
||||
assert "if (level) bars.setTools(level.tools);" in src # 229-1 단추 = 보조 탭 줄
|
||||
assert (
|
||||
"panel.replaceChildren(table.root, ...(foot ? [foot] : []))" in src
|
||||
"panel.replaceChildren(...banner, table.root, ...(fold ? [fold] : []), ...(foot ? [foot] : []))"
|
||||
in src
|
||||
) # 203 레벨 안내 + 탭 안내 = 한 영역
|
||||
lvl = (B09 / "B09_Estimation_UI_Tabs_Bill_Level.ts").read_text("utf-8")
|
||||
assert "table.menu(index)" in lvl # 단추 · 키 = 우클릭 메뉴 같은 함수
|
||||
|
||||
@@ -238,6 +238,7 @@ def test_화면_출처_다섯_사전조합_창_들여오기_줄_연결_TS():
|
||||
"key": "SS000011"} # fmt: skip
|
||||
ref = {"tab": "dg", "key": "DG:new", "lib": "precombo@company@7@SC-000002"}
|
||||
assert out["applied"][0] == [ref, "회사 조합"]
|
||||
# 원가 자료만 바꿔 끼움 — 사용자 값(서버 user)은 안 받음
|
||||
assert "user" not in out["payload"] and out["payload"]["inputs"] == {"daega": {"libs": {}}}
|
||||
# 원가 자료 바꿔 끼움 — 702-3 user = 서버가 그 셈에 쓴 기준(화면 캐시는 그대로 · 다른 로직 줄 = 뒤 셈)
|
||||
assert out["payload"]["user"] == {"bill": "서버 것"}
|
||||
assert out["payload"]["inputs"] == {"daega": {"libs": {}}}
|
||||
assert out["toasts"][-1][0] == "success"
|
||||
|
||||
@@ -63,8 +63,8 @@ def test_B08_다음_단계_완료_뒤_이동_실패면_머묾(r):
|
||||
assert p["fail"] == ["req:POST:/projects/P1/quantity/complete", "toast:error"]
|
||||
|
||||
|
||||
def test_B09_단추_줄_초기화_하나_저장_없음(r):
|
||||
assert r["b09"]["dock"] == ["danger-ghost"]
|
||||
def test_B09_단추_줄_저장_초기화(r):
|
||||
assert r["b09"]["dock"] == ["filled", "danger-ghost"] # 702-3 [저장] = B08 과 같은 꼴 · 자리
|
||||
assert r["b09"]["nextEnabled"] is True
|
||||
|
||||
|
||||
@@ -74,6 +74,7 @@ def test_B09_다음_저장_완료_인쇄(r):
|
||||
assert b["changed"] == [
|
||||
True,
|
||||
[
|
||||
"ask", # 702-3 저장 안 한 고침 = 물음([저장] 고름)
|
||||
"lock",
|
||||
"fetch:PUT:/api/projects/P1/estimation",
|
||||
"unlock",
|
||||
@@ -101,7 +102,11 @@ def test_B09_초기화(r):
|
||||
|
||||
|
||||
def test_B09_초안_없으면_다음_흐림(r):
|
||||
assert r["b09NoDraft"] == {"dock": ["danger-ghost"], "nextEnabled": False, "nav": [False, []]}
|
||||
assert r["b09NoDraft"] == {
|
||||
"dock": ["filled", "danger-ghost"],
|
||||
"nextEnabled": False,
|
||||
"nav": [False, []],
|
||||
}
|
||||
|
||||
|
||||
def test_초안_다음_길_등록():
|
||||
|
||||
@@ -98,6 +98,6 @@ def test_잇기():
|
||||
groups = (ROOT / GROUPS).read_text("utf-8")
|
||||
assert "toggle" not in groups and "redraw" not in groups and "createButton" not in groups
|
||||
shell = (ROOT / "B09_Estimation/B09_Estimation_UI_Shell.ts").read_text("utf-8")
|
||||
assert "createDock([reset], () => isGroupTab(current) && show(current))" in shell
|
||||
assert "createDock([save, reset], () => isGroupTab(current) && show(current))" in shell
|
||||
state = (ROOT / "A00_Common/b_page_state.ts").read_text("utf-8")
|
||||
assert '"b09-group-view": { bucket: "pref", scope: "global" },' in state
|
||||
|
||||
@@ -59,12 +59,16 @@ def test_B08_좌측_옵션_save(r):
|
||||
assert o["keptOnFail"] is True
|
||||
|
||||
|
||||
def test_B09_고친_값_저장_성공_뒤_이동(r):
|
||||
def test_B09_고친_값_있으면_물음_뒤_이동(r):
|
||||
# 702-3 B09 [저장] 단추 생김 — 단계 이동 = 저장 안 한 고침 있으면 물음(저장 · 버림 · 머묾)
|
||||
b = r["b09"]
|
||||
assert b["beforeLoad"] == ["go:b08"] # 불러오기 전 = 저장 없이(캐시 남음)
|
||||
assert b["beforeLoad"] == ["go:b08"] # 불러오기 전 = 물음 · 저장 없이(캐시 남음)
|
||||
assert b["clean"] == ["go:b08"] # 고침 없음 = 요청 없이
|
||||
assert b["fail"] == ["fetch:PUT:/api/projects/P1/estimation"] # 실패 = 머묾
|
||||
assert b["ok"] == ["fetch:PUT:/api/projects/P1/estimation", "go:b08"]
|
||||
put = "fetch:PUT:/api/projects/P1/estimation"
|
||||
assert b["fail"] == ["ask", put] # [저장] 실패 = 머묾
|
||||
assert b["ok"] == ["ask", put, "go:b08"]
|
||||
assert b["discard"] == [["ask", "go:b08"], False] # [버림] = 캐시 지움 · 저장 없이
|
||||
assert b["stay"] == [["ask"], True]
|
||||
|
||||
|
||||
def test_자재_수리_탭_저장_등록():
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
임시 층(견본 마스터 · 임시 storage) · 실제 마스터 · 프로젝트는 안 씀.
|
||||
"""
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
@@ -43,3 +45,38 @@ def test_서고_사본_산근_목록에_듦_대가_단가_채움(world):
|
||||
labor = compute_labor(daega, {**base, "gaps": []}, by_key)
|
||||
row = next(r for r in labor["daega"] if r["key"] == block["id"])
|
||||
assert row["sum"] and not row.get("blank")
|
||||
|
||||
|
||||
def test_화면_연결_뒤_셈_저장_단추_옮김_물음_TS():
|
||||
shell = (B09 / "B09_Estimation_UI_Shell.ts").read_text("utf-8")
|
||||
live = (B09 / "B09_Estimation_UI_Live.ts").read_text("utf-8")
|
||||
router = (B09 / "B09_Estimation_Router.py").read_text("utf-8")
|
||||
assert '"/{project_id}/estimation/calc"' in router
|
||||
assert "AbortController" in live and "seq" in live # 차례 번호 · 옛 요청 끊기
|
||||
assert "Common_Btn_Save" in shell and "b09Save" in shell
|
||||
assert "askProjectLeave" in shell # 단계 이동 = 물음(저장 · 폐기 · 취소)
|
||||
|
||||
|
||||
def test_뒤_셈_차례_번호_옛_요청_끊기_셈_중():
|
||||
proc = subprocess.run(
|
||||
["node", str(Path(__file__).parent / "helper_702_3_live.cjs")],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
timeout=60,
|
||||
)
|
||||
assert proc.returncode == 0, proc.stderr
|
||||
o = json.loads(proc.stdout)
|
||||
assert o["wait"] == ["hand:a"] and o["same"] == [] # 로직 연결이 서버 기준과 다른 줄만
|
||||
assert o["strip"] == {} # 기다리는 줄 = 옛 산근 단가 안 씀
|
||||
assert o["mark"] == ["셈 중", None] # 그 줄 금액 칸만
|
||||
assert o["sentAfterSame"] == 1 # 가는 중 같은 지문 = 다시 안 보냄
|
||||
assert o["firstAborted"] is True # 바뀐 지문 = 옛 요청 끊음
|
||||
assert o["url"] == "/api/projects/P1/estimation/calc"
|
||||
assert o["settled"] == [[2, 2]] and o["busyAfter"] is False # 마지막 것만 받음
|
||||
assert o["cancelled"] == [True, False] # 기다릴 줄 없어짐 = 가던 것 끊음
|
||||
assert o["failed"] == [
|
||||
["warning"],
|
||||
[None, 4],
|
||||
4,
|
||||
] # 못 받음 = 알림 한 번 · 같은 지문 다시 안 보냄
|
||||
|
||||
Reference in New Issue
Block a user