feat(780-2): 엑셀 보기 단가산출 탭 오른쪽 = 단가산출 목록 — 판 바꾸기 · 거르기 · 목록 ↔ 시트 · 두 벌 맞춤(요구 627)
- 새 Hopyo_SheetSk: 탭(역할) 따라 오른쪽 판 바꿈 · 일위대가표 = 호표 목록 그대로 · 단가산출표 · 상세 = 마법사 단가산출 칸 좁은 꼴 한 벌(처음 올 때 지음 · 다른 층 · 권한 없음 = 보기만) · 시트 칸 → 목록 강조 · 목록 줄 → 지금 탭 블록 자리 - WizardSk: 좁은 꼴 옵션 · 부름 칸(일위대가 n 곳 · 일위대가 밖) · 거르기(전체 · 안 · 밖 · 고를 줄 있음) · 같은 캐시 두 벌 맞춤 알림 · 옮김 뒤 HOPYO_BUILT(열린 엑셀 보기 칠 다시) - SkPick: 줄 0 블록 = 「줄 없음」 확인 막음 · 거르기 · 끝내기 글 재료에 일위대가 밖 수 - SheetNav: role · blockAt · spotOf 내보냄 · SheetMap: 판정 없음 글 → 「일위대가 밖」 - Wizard [끝내기]: 막지 않음 · 물음창에 안 옮긴 단가산출 n(일위대가 밖 m) - 시험: 새 test_627_sheet_sk(판 바꿈 · 목록 지음 · 줄 0 막음 · 캐시 공유) · test_778_5 끝내기 재료 꼴에 밖 칸 더함 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -284,6 +284,31 @@
|
|||||||
min-height: 0;
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 단가산출 탭 오른쪽 = 단가산출 목록(780-2) — 판 넓힘 · 숨김이 display 에 안 덮이게 · 강조 줄 */
|
||||||
|
.m02-hopyo-sheet.is-sk {
|
||||||
|
grid-template-columns: minmax(0, 1fr) minmax(22rem, 42%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.m02-hopyo-sheet__side[hidden] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.m02-hopyo-sheet__sk {
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.m02-hopyo-sheet__sk tr[data-sk-block] {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.m02-hopyo-sheet__sk tr.is-current > td {
|
||||||
|
background: var(--color-accent-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.m02-hopyo-sheet__sk tr.is-current > td:first-child {
|
||||||
|
box-shadow: inset 3px 0 0 var(--color-accent);
|
||||||
|
}
|
||||||
|
|
||||||
.m02-hopyo-sheet__list {
|
.m02-hopyo-sheet__list {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
|
|||||||
@@ -4,7 +4,8 @@
|
|||||||
* 제안 n / 줄 m · 숫자 판정 셈 중 · [삭제](좌측 파일 줄 누름으로 바로 엶 · 옛 본 칸 표 몫) ·
|
* 제안 n / 줄 m · 숫자 판정 셈 중 · [삭제](좌측 파일 줄 누름으로 바로 엶 · 옛 본 칸 표 몫) ·
|
||||||
* 왼쪽 = 올린 엑셀을 연 것처럼 읽기 전용 시트(A00 산출근거 스프레드시트 · 탭 셋 일위대가표 · 단가산출표 · 단가산출 상세 ·
|
* 왼쪽 = 올린 엑셀을 연 것처럼 읽기 전용 시트(A00 산출근거 스프레드시트 · 탭 셋 일위대가표 · 단가산출표 · 단가산출 상세 ·
|
||||||
* 칠 · 범례 · 뛰기 · 뒤로 = `Hopyo_SheetNav` · 779-4 · 같은 파일 창은 다시 씀 = 앞으로 꺼내 그 자리로 · 779-6) ·
|
* 칠 · 범례 · 뛰기 · 뒤로 = `Hopyo_SheetNav` · 779-4 · 같은 파일 창은 다시 씀 = 앞으로 꺼내 그 자리로 · 779-6) ·
|
||||||
* 오른쪽 = 호표 목록(체크 · 제N호표 [수정] · 찾기 · 제안 n/m — 다 연결이면 녹색 · 뒤 셈 중이면 3 초마다 다시) + [저장].
|
* 오른쪽 = 호표 목록(체크 · 제N호표 [수정] · 찾기 · 제안 n/m — 다 연결이면 녹색 · 뒤 셈 중이면 3 초마다 다시) + [저장] ·
|
||||||
|
* 단가산출표 · 상세 탭이면 단가산출 목록으로 바뀜(780-2 · `Hopyo_SheetSk`).
|
||||||
* · 목록 누름 → 시트에서 그 호표 영역(B열 ~ 끝 열) 고름 · 칸 누름 → 그 행 호표를 목록에서 강조 · 머리 행이면 영역 고름
|
* · 목록 누름 → 시트에서 그 호표 영역(B열 ~ 끝 열) 고름 · 칸 누름 → 그 행 호표를 목록에서 강조 · 머리 행이면 영역 고름
|
||||||
* · [수정] = 그 호표 확인 창(`openHopyoCheck` · 줄 누름과 따로)
|
* · [수정] = 그 호표 확인 창(`openHopyoCheck` · 줄 누름과 따로)
|
||||||
* · [저장] = 체크한 호표 + 확인 안 된 하위 호표(777-1 · `Hopyo_Kids`)마다 체크 줄(확인 창 캐시 · 없으면 서버 제안)로
|
* · [저장] = 체크한 호표 + 확인 안 된 하위 호표(777-1 · `Hopyo_Kids`)마다 체크 줄(확인 창 캐시 · 없으면 서버 제안)로
|
||||||
@@ -55,6 +56,7 @@ import {
|
|||||||
import { bodyPair, kidsText, leftAt, readyFor, unmadeKids } from "./M02_MasterTemplete_Hopyo_Kids";
|
import { bodyPair, kidsText, leftAt, readyFor, unmadeKids } from "./M02_MasterTemplete_Hopyo_Kids";
|
||||||
import { builtWord } from "./M02_MasterTemplete_Hopyo_Choose";
|
import { builtWord } from "./M02_MasterTemplete_Hopyo_Choose";
|
||||||
import * as Nav from "./M02_MasterTemplete_Hopyo_SheetNav";
|
import * as Nav from "./M02_MasterTemplete_Hopyo_SheetNav";
|
||||||
|
import { createSheetSk, type SheetSk } from "./M02_MasterTemplete_Hopyo_SheetSk";
|
||||||
import { openRelink, relinkKeys } from "./M02_MasterTemplete_Hopyo_Relink";
|
import { openRelink, relinkKeys } from "./M02_MasterTemplete_Hopyo_Relink";
|
||||||
import { leftButton } from "./M02_MasterTemplete_Hopyo_Left";
|
import { leftButton } from "./M02_MasterTemplete_Hopyo_Left";
|
||||||
import { formatPrice } from "../common_util/common_util_money";
|
import { formatPrice } from "../common_util/common_util_money";
|
||||||
@@ -146,6 +148,7 @@ export function openHopyoSheet(opt: HopyoSheetOptions): void {
|
|||||||
if (opened.has(id)) return opened.get(id)!(opt);
|
if (opened.has(id)) return opened.get(id)!(opt);
|
||||||
let sheet: SpreadsheetHandle | null = null;
|
let sheet: SpreadsheetHandle | null = null;
|
||||||
let nav: Nav.SheetNav | null = null;
|
let nav: Nav.SheetNav | null = null;
|
||||||
|
let sk: SheetSk | null = null; // 단가산출 탭 오른쪽 판(780-2)
|
||||||
let detail = ""; // 단가산출 상세 시트 id
|
let detail = ""; // 단가산출 상세 시트 id
|
||||||
let above: Element[] = []; // 앞으로 꺼냈을 때 원래 위에 있던 창
|
let above: Element[] = []; // 앞으로 꺼냈을 때 원래 위에 있던 창
|
||||||
let closed = false;
|
let closed = false;
|
||||||
@@ -369,12 +372,14 @@ export function openHopyoSheet(opt: HopyoSheetOptions): void {
|
|||||||
const more = { ...opt, target, grid, last: lastOf, sheet: () => sheet, host: left };
|
const more = { ...opt, target, grid, last: lastOf, sheet: () => sheet, host: left };
|
||||||
nav = ids.일위대가표 ? Nav.createSheetNav(more) : null;
|
nav = ids.일위대가표 ? Nav.createSheetNav(more) : null;
|
||||||
if (nav) warn.after(nav.root);
|
if (nav) warn.after(nav.root);
|
||||||
|
sk = nav ? createSheetSk({ ...opt, target, nav, side: right }) : null;
|
||||||
sheet = createSpreadsheet(left, book, {
|
sheet = createSpreadsheet(left, book, {
|
||||||
readOnly: true,
|
readOnly: true,
|
||||||
칠: nav?.fill,
|
칠: nav?.fill,
|
||||||
onSelect: (s) => {
|
onSelect: (s) => {
|
||||||
at = s;
|
at = s;
|
||||||
nav?.at(s);
|
nav?.at(s);
|
||||||
|
sk?.at(s);
|
||||||
if (s.시트 !== ilwi) return;
|
if (s.시트 !== ilwi) return;
|
||||||
const no = hopyoAtRow(spans, Number(/\d+/.exec(s.칸)?.[0]));
|
const no = hopyoAtRow(spans, Number(/\d+/.exec(s.칸)?.[0]));
|
||||||
if (no !== null && no !== current) mark(no);
|
if (no !== null && no !== current) mark(no);
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
* · 779-6 — 마지막 격자 다시 씀(`gridOf`) · 열린 창 앞으로 꺼냄 · 닫기 = 제자리로(`raise` · `lower`)
|
* · 779-6 — 마지막 격자 다시 씀(`gridOf`) · 열린 창 앞으로 꺼냄 · 닫기 = 제자리로(`raise` · `lower`)
|
||||||
* · 778-5 D-6 — 머리 막대 아래 띠(부품 `createBanner`) 「엑셀 셈 어긋남 n」 + [보기 i/m] = 상세 시트 그 행으로 차례로 뛰기 ·
|
* · 778-5 D-6 — 머리 막대 아래 띠(부품 `createBanner`) 「엑셀 셈 어긋남 n」 + [보기 i/m] = 상세 시트 그 행으로 차례로 뛰기 ·
|
||||||
* 그 행 까닭은 막대 글에 덧붙임 · 칠은 안 바꿈
|
* 그 행 까닭은 막대 글에 덧붙임 · 칠은 안 바꿈
|
||||||
|
* · 780-2 — `role(시트)` · `blockAt(시트, 행)`(단가산출표 목록 행 · 상세 블록 범위) · `spotOf(번, 역할)` 내보냄(오른쪽 단가산출 목록)
|
||||||
* 블록 재료 = 서버 `GET …/hopyo-marks/{키}`(779-2 · 읽기만).
|
* 블록 재료 = 서버 `GET …/hopyo-marks/{키}`(779-2 · 읽기만).
|
||||||
* ========================================================================== */
|
* ========================================================================== */
|
||||||
|
|
||||||
@@ -32,6 +33,7 @@ import {
|
|||||||
indexOf,
|
indexOf,
|
||||||
jumpsAt,
|
jumpsAt,
|
||||||
missesOf,
|
missesOf,
|
||||||
|
numAt,
|
||||||
paintOf,
|
paintOf,
|
||||||
ROLES,
|
ROLES,
|
||||||
type BlockMark,
|
type BlockMark,
|
||||||
@@ -40,6 +42,7 @@ import {
|
|||||||
type Jump,
|
type Jump,
|
||||||
type Mark,
|
type Mark,
|
||||||
type Role,
|
type Role,
|
||||||
|
type Spot,
|
||||||
} from "./logic/M02_MasterTemplete_Hopyo_SheetMap";
|
} from "./logic/M02_MasterTemplete_Hopyo_SheetMap";
|
||||||
|
|
||||||
/** 확인 창 캐시를 고쳤음(document 알림 · `Hopyo_Check.saveDraft` 가 같은 글로 냄) — 엑셀 보기 창 다시 칠 */
|
/** 확인 창 캐시를 고쳤음(document 알림 · `Hopyo_Check.saveDraft` 가 같은 글로 냄) — 엑셀 보기 창 다시 칠 */
|
||||||
@@ -265,6 +268,21 @@ export function createSheetNav(opt: SheetNavOptions) {
|
|||||||
void readBlocks();
|
void readBlocks();
|
||||||
},
|
},
|
||||||
jumpTo,
|
jumpTo,
|
||||||
|
/** 시트 id → 역할(세 보기 밖 = undefined) */
|
||||||
|
role: (sheet: string): Role | undefined => roleOf.get(sheet),
|
||||||
|
/** 그 시트 · 행의 단산 블록 번 — 단가산출표 = 목록 행 · 상세 = 블록 범위 · 아니면 null */
|
||||||
|
blockAt(sheet: string, row: number): number | null {
|
||||||
|
if (sheet === ix.ids.단가산출표) return ix.listAt.get(row) ?? null;
|
||||||
|
if (sheet === ix.ids.단가산출상세) return numAt(ix.block, row);
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
/** 그 블록 자리 — 단가산출표 = 목록 행 · 상세 = 블록 범위 · 없으면 null */
|
||||||
|
spotOf(no: number, role: Role): Spot | null {
|
||||||
|
const sheet = ix.ids[role];
|
||||||
|
const at = role === "단가산출표" ? ix.listRow.get(no) : undefined;
|
||||||
|
const rows: [number, number] | undefined = at ? [at, at] : role === "단가산출상세" ? ix.block.get(no) : undefined; // prettier-ignore
|
||||||
|
return sheet && rows ? { 시트: sheet, 행: rows } : null;
|
||||||
|
},
|
||||||
destroy(): void {
|
destroy(): void {
|
||||||
document.removeEventListener(HOPYO_DRAFT, onDraft);
|
document.removeEventListener(HOPYO_DRAFT, onDraft);
|
||||||
document.removeEventListener(HOPYO_BUILT, onBuilt);
|
document.removeEventListener(HOPYO_BUILT, onBuilt);
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
/* =============================================================================
|
||||||
|
* M02_MasterTemplete_Hopyo_SheetSk.ts
|
||||||
|
* 엑셀 보기 창 오른쪽 판 바꾸기(PLAN 780-2 · 요구 627) — 탭 따라:
|
||||||
|
* · 일위대가표 = 호표 목록(`Hopyo_Sheet` 판 그대로) · 단가산출표 · 단가산출 상세 = 단가산출 목록
|
||||||
|
* (마법사 단가산출 칸 `sangeunStep` 좁은 꼴 한 벌 · 같은 캐시 · 같은 [확인] → [확인한 것 옮기기] · 마법사 체크와 상관없음)
|
||||||
|
* · 목록 블록 줄 누름 → 지금 탭의 그 블록 자리(단가산출표 = 목록 행 · 상세 = 블록 범위 · [◂ 뒤로] 쌓임)
|
||||||
|
* · 시트 칸 고름 → 그 행 블록을 목록에서 강조(`SheetNav.blockAt`)
|
||||||
|
* · 단가산출 칸은 처음 단가산출 탭에 올 때 지음(판정 셈이 무거움) · 만들 층 ≠ 파일 층 · 쓰기 권한 없음 = 보기만
|
||||||
|
* ========================================================================== */
|
||||||
|
|
||||||
|
import { el } from "@ui/ui_template_elements";
|
||||||
|
import type { HopyoScope } from "./M02_MasterTemplete_Hopyo_Api";
|
||||||
|
import type { SheetNav } from "./M02_MasterTemplete_Hopyo_SheetNav";
|
||||||
|
import { sangeunStep, type SkStep } from "./M02_MasterTemplete_Hopyo_WizardSk";
|
||||||
|
|
||||||
|
export interface SheetSkOptions {
|
||||||
|
scope: HopyoScope;
|
||||||
|
key: string;
|
||||||
|
파일: string;
|
||||||
|
target: HopyoScope;
|
||||||
|
canWrite: boolean;
|
||||||
|
nav: SheetNav;
|
||||||
|
/** 호표 목록 판 — 단가산출 탭이면 숨김 · 그 뒤에 단가산출 판을 붙임 */
|
||||||
|
side: HTMLElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sameLayer = (a: HopyoScope, b: HopyoScope): boolean =>
|
||||||
|
a.layer === b.layer && (a.owner ?? null) === (b.owner ?? null);
|
||||||
|
|
||||||
|
export function createSheetSk(opt: SheetSkOptions) {
|
||||||
|
const root = el("div", { className: "m02-hopyo-sheet__side m02-hopyo-sheet__sk" });
|
||||||
|
root.setAttribute("data-hopyo-sk-side", "");
|
||||||
|
root.hidden = true;
|
||||||
|
opt.side.after(root);
|
||||||
|
let step: SkStep | null = null;
|
||||||
|
let sheet = "";
|
||||||
|
|
||||||
|
/** 블록 줄 누름 — 지금 탭에서 그 블록 자리 · 없으면 다른 단가산출 탭 */
|
||||||
|
function onRow(no: number): void {
|
||||||
|
const list = opt.nav.role(sheet) === "단가산출표";
|
||||||
|
const spot = list
|
||||||
|
? (opt.nav.spotOf(no, "단가산출표") ?? opt.nav.spotOf(no, "단가산출상세"))
|
||||||
|
: (opt.nav.spotOf(no, "단가산출상세") ?? opt.nav.spotOf(no, "단가산출표"));
|
||||||
|
if (spot) opt.nav.jumpTo(spot.시트, spot.행);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
root,
|
||||||
|
/** 시트 고름 · 탭 바꿈 — 판 바꾸기 · 강조 */
|
||||||
|
at(sel: { 시트: string; 칸: string }): void {
|
||||||
|
sheet = sel.시트;
|
||||||
|
const role = opt.nav.role(sel.시트);
|
||||||
|
const sk = role === "단가산출표" || role === "단가산출상세";
|
||||||
|
opt.side.hidden = sk;
|
||||||
|
root.hidden = !sk;
|
||||||
|
opt.side.parentElement?.classList.toggle("is-sk", sk);
|
||||||
|
if (!sk) return;
|
||||||
|
if (!step) {
|
||||||
|
const readOnly = !opt.canWrite || !sameLayer(opt.scope, opt.target);
|
||||||
|
step = sangeunStep(opt.scope, opt.key, opt.파일, undefined, { narrow: true, onRow, readOnly }); // prettier-ignore
|
||||||
|
root.append(step.root);
|
||||||
|
}
|
||||||
|
step.mark(opt.nav.blockAt(sel.시트, Number(/\d+/.exec(sel.칸)?.[0] ?? 0)));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SheetSk = ReturnType<typeof createSheetSk>;
|
||||||
@@ -288,9 +288,13 @@ export function buildHopyoWizard(): HopyoWizard {
|
|||||||
const loose = [...got.values()].filter(
|
const loose = [...got.values()].filter(
|
||||||
(x) => !x.one.호표.만든?.확인 && hasDraft(draftKey(scope, file.키, x.one.호표.번)),
|
(x) => !x.one.호표.만든?.확인 && hasDraft(draftKey(scope, file.키, x.one.호표.번)),
|
||||||
).length;
|
).length;
|
||||||
|
const w = sk?.count(); // 안 옮긴 단가산출(일위대가 밖 포함) — 묻기만
|
||||||
const ok = await showConfirmDialog(
|
const ok = await showConfirmDialog(
|
||||||
`「${file.파일}」 불러오기를 끝냄 — 올린 파일(엑셀 결과 · 격자 · 맞춤 캐시)을 지움 · 만든 일위대가 · 단가산출(출처만 남김)과 별칭 장부는 남음 · 같은 엑셀은 필요하면 다시 올림` +
|
`「${file.파일}」 불러오기를 끝냄 — 올린 파일(엑셀 결과 · 격자 · 맞춤 캐시)을 지움 · 만든 일위대가 · 단가산출(출처만 남김)과 별칭 장부는 남음 · 같은 엑셀은 필요하면 다시 올림` +
|
||||||
(loose ? ` · ${L("M02_LeftLoose").replace("{n}", String(loose))}` : ""),
|
(loose ? ` · ${L("M02_LeftLoose").replace("{n}", String(loose))}` : ""),
|
||||||
|
w?.블록
|
||||||
|
? ` · ${L("M02_SkUnmoved").replace("{n}", String(w.블록)).replace("{m}", String(w.밖))}`
|
||||||
|
: "", // 780-2 막지 않음
|
||||||
"끝내기",
|
"끝내기",
|
||||||
);
|
);
|
||||||
if (!ok) return steps.set("2");
|
if (!ok) return steps.set("2");
|
||||||
|
|||||||
@@ -7,7 +7,10 @@
|
|||||||
* · 고름 · 확인 = 캐시 `m02-hopyo-sk:{층}:{주인}:{키}`(`SkPick` 꼴) — 누를 때 서버에 안 감 · 자동 저장 없음
|
* · 고름 · 확인 = 캐시 `m02-hopyo-sk:{층}:{주인}:{키}`(`SkPick` 꼴) — 누를 때 서버에 안 감 · 자동 저장 없음
|
||||||
* · [확인한 것 옮기기 (n)] = 몸 `{확인: [{번, 고름}]}` → 서버가 M01 지금 값으로 다시 셈 · 단위 SK(확인 안 한 블록 안 만듦) ·
|
* · [확인한 것 옮기기 (n)] = 몸 `{확인: [{번, 고름}]}` → 서버가 M01 지금 값으로 다시 셈 · 단위 SK(확인 안 한 블록 안 만듦) ·
|
||||||
* 만든 블록은 확인에서 뺌 · 못만듦 = 띠(까닭 그대로) · 「모두 확인」 단추 없음
|
* 만든 블록은 확인에서 뺌 · 못만듦 = 띠(까닭 그대로) · 「모두 확인」 단추 없음
|
||||||
* · `count()` = 아직 안 옮긴 블록 · 고를 줄(마법사 [끝내기] 막힘 글)
|
* · `count()` = 아직 안 옮긴 블록 · 그중 일위대가 밖 · 고를 줄(마법사 [끝내기] 글)
|
||||||
|
* · 780-2(요구 627) — 부름 칸(「일위대가 n 곳」 · 「일위대가 밖」) · 거르기(전체 · 안 · 밖 · 고를 줄 있음) ·
|
||||||
|
* 좁은 꼴(`narrow` = 엑셀 보기 오른쪽 · 칸 여섯 · 블록 줄 누름 = `onRow` · `mark(번)` 강조) · `readOnly` = 옮기기 숨김 ·
|
||||||
|
* 두 벌 맞춤 = 알림 `SK_SYNC`(같은 캐시 열쇠 · 고름 바뀜 → 다시 그림 · 옮김 → 받은 판정 그대로) · 옮김 뒤 `HOPYO_BUILT`
|
||||||
* ========================================================================== */
|
* ========================================================================== */
|
||||||
|
|
||||||
import { createBadge } from "@ui/ui_template_badge";
|
import { createBadge } from "@ui/ui_template_badge";
|
||||||
@@ -17,6 +20,8 @@ import { t as L } from "@ui/ui_template_locale";
|
|||||||
import { tableRow, tableShell, type Column } from "@ui/ui_template_table";
|
import { tableRow, tableShell, type Column } from "@ui/ui_template_table";
|
||||||
import { createText } from "@ui/ui_template_text";
|
import { createText } from "@ui/ui_template_text";
|
||||||
import { createToggle } from "@ui/ui_template_toggle";
|
import { createToggle } from "@ui/ui_template_toggle";
|
||||||
|
import { createSegment } from "@ui/ui_template_segment";
|
||||||
|
import { HOPYO_BUILT } from "./M02_MasterTemplete_Hopyo_Check";
|
||||||
import { formatPrice } from "../common_util/common_util_money";
|
import { formatPrice } from "../common_util/common_util_money";
|
||||||
import type { HopyoScope } from "./M02_MasterTemplete_Hopyo_Api";
|
import type { HopyoScope } from "./M02_MasterTemplete_Hopyo_Api";
|
||||||
import { judgeSangeun } from "./M02_MasterTemplete_Hopyo_SkApi";
|
import { judgeSangeun } from "./M02_MasterTemplete_Hopyo_SkApi";
|
||||||
@@ -24,6 +29,8 @@ import { skLineTable } from "./M02_MasterTemplete_Hopyo_SkLines";
|
|||||||
import {
|
import {
|
||||||
GOOD,
|
GOOD,
|
||||||
canConfirm,
|
canConfirm,
|
||||||
|
filterBlocks,
|
||||||
|
outside,
|
||||||
picksOf,
|
picksOf,
|
||||||
readDraft,
|
readDraft,
|
||||||
setPick,
|
setPick,
|
||||||
@@ -34,6 +41,7 @@ import {
|
|||||||
waiting,
|
waiting,
|
||||||
type SkBlock,
|
type SkBlock,
|
||||||
type SkDraft,
|
type SkDraft,
|
||||||
|
type SkFilter,
|
||||||
type SkJudged,
|
type SkJudged,
|
||||||
} from "./logic/M02_MasterTemplete_Hopyo_SkPick";
|
} from "./logic/M02_MasterTemplete_Hopyo_SkPick";
|
||||||
|
|
||||||
@@ -46,26 +54,63 @@ export const skDraftKey = (scope: HopyoScope, key: string): string =>
|
|||||||
|
|
||||||
export interface SkStep {
|
export interface SkStep {
|
||||||
root: HTMLElement;
|
root: HTMLElement;
|
||||||
/** 아직 안 옮긴 블록 · 고를 줄 — 판정 전 null */
|
/** 아직 안 옮긴 블록 · 그중 일위대가 밖 · 고를 줄 — 판정 전 null */
|
||||||
count: () => { 블록: number; 줄: number } | null;
|
count: () => { 블록: number; 밖: number; 줄: number } | null;
|
||||||
/** 그 블록 펼치고 화면에 보임(안 이은 줄 창 [단가산출 걸음에서 확인]) */
|
/** 그 블록 펼치고 화면에 보임(안 이은 줄 창 [단가산출 걸음에서 확인]) */
|
||||||
show: (번: number) => void;
|
show: (번: number) => void;
|
||||||
|
/** 그 블록 줄 강조 · 보이게(펼치지 않음 · 거른 줄이면 거르기 풂) — null = 강조 지움 */
|
||||||
|
mark: (번: number | null) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SkStepOptions {
|
||||||
|
/** 좁은 꼴(엑셀 보기 오른쪽) — 칸 여섯 · 금액은 줄 title */
|
||||||
|
narrow?: boolean;
|
||||||
|
/** 블록 줄 누름(단추 밖) */
|
||||||
|
onRow?: (번: number) => void;
|
||||||
|
/** 옮기기 숨김(만들 층 ≠ 파일 층 · 쓰기 권한 없음) */
|
||||||
|
readOnly?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 두 벌(마법사 칸 · 엑셀 보기 오른쪽) 맞춤 알림 — detail `{dk, from, got?}` */
|
||||||
|
export const SK_SYNC = "m02-hopyo-sk";
|
||||||
|
type SyncDetail = { dk: string; from: object; got?: SkJudged };
|
||||||
|
|
||||||
/** ② 단가산출 칸 — `파일` = 엑셀 보기 창 머리 · `onChange` = 판정 · 옮김 뒤(끝내기 글) */
|
/** ② 단가산출 칸 — `파일` = 엑셀 보기 창 머리 · `onChange` = 판정 · 옮김 뒤(끝내기 글) */
|
||||||
export function sangeunStep(
|
export function sangeunStep(
|
||||||
scope: HopyoScope,
|
scope: HopyoScope,
|
||||||
key: string,
|
key: string,
|
||||||
파일: string,
|
파일: string,
|
||||||
onChange?: () => void,
|
onChange?: () => void,
|
||||||
|
o: SkStepOptions = {},
|
||||||
): SkStep {
|
): SkStep {
|
||||||
const dk = skDraftKey(scope, key);
|
const dk = skDraftKey(scope, key);
|
||||||
|
const me = {};
|
||||||
const load = (): SkDraft => readDraft(sessionStorage.getItem(dk));
|
const load = (): SkDraft => readDraft(sessionStorage.getItem(dk));
|
||||||
const save = (d: SkDraft): void => sessionStorage.setItem(dk, JSON.stringify(d));
|
const save = (d: SkDraft): void => sessionStorage.setItem(dk, JSON.stringify(d));
|
||||||
|
const tell = (more: Partial<SyncDetail> = {}): void =>
|
||||||
|
void document.dispatchEvent(new CustomEvent<SyncDetail>(SK_SYNC, { detail: { dk, from: me, ...more } })); // prettier-ignore
|
||||||
let got: SkJudged | null = null;
|
let got: SkJudged | null = null;
|
||||||
let fails: SkJudged["못만듦"] = [];
|
let fails: SkJudged["못만듦"] = [];
|
||||||
|
let current: number | null = null; // 강조 블록
|
||||||
|
let filter: SkFilter = "all";
|
||||||
const open = new Set<number>(); // 펼친 블록
|
const open = new Set<number>(); // 펼친 블록
|
||||||
const out = el("div", { className: "m02-hopyo", attrs: { "data-wiz-sk": "" } });
|
const out = el("div", { className: "m02-hopyo", attrs: { "data-wiz-sk": "" } });
|
||||||
|
const seg = createSegment({
|
||||||
|
options: [
|
||||||
|
{ value: "all", label: L("M02_SkFilterAll") },
|
||||||
|
{ value: "in", label: L("M02_SkFilterIn") },
|
||||||
|
{ value: "out", label: L("M02_SkFilterOut") },
|
||||||
|
{ value: "open", label: L("M02_SkFilterOpen") },
|
||||||
|
],
|
||||||
|
value: filter,
|
||||||
|
size: 24,
|
||||||
|
ariaLabel: L("M02_SkFilter"),
|
||||||
|
onChange: (v) => {
|
||||||
|
filter = v as SkFilter;
|
||||||
|
draw();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
seg.root.dataset.skFilter = "";
|
||||||
const again = createButton({ label: L("M02_SkJudge"), variant: "ghost", size: 28, onClick: () => void judge() }); // prettier-ignore
|
const again = createButton({ label: L("M02_SkJudge"), variant: "ghost", size: 28, onClick: () => void judge() }); // prettier-ignore
|
||||||
again.dataset.skJudge = "";
|
again.dataset.skJudge = "";
|
||||||
const move = createButton({ label: L("M02_SkMove").replace("{n}", "0"), variant: "filled", size: 28, disabled: true, onClick: () => void run() }); // prettier-ignore
|
const move = createButton({ label: L("M02_SkMove").replace("{n}", "0"), variant: "filled", size: 28, disabled: true, onClick: () => void run() }); // prettier-ignore
|
||||||
@@ -74,11 +119,24 @@ export function sangeunStep(
|
|||||||
const root = el("div", {
|
const root = el("div", {
|
||||||
className: "m02-hopyo",
|
className: "m02-hopyo",
|
||||||
children: [
|
children: [
|
||||||
createText(L("M02_SkHint"), "hint", 14),
|
...(o.narrow ? [] : [createText(L("M02_SkHint"), "hint", 14)]),
|
||||||
|
seg.root,
|
||||||
out,
|
out,
|
||||||
el("div", { className: "ui-toolbar", children: [move, again] }),
|
el("div", { className: "ui-toolbar", children: o.readOnly ? [again] : [move, again] }),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
/** 다른 벌이 고름 · 옮김을 알림 — 떨어진 벌은 듣기 그침 */
|
||||||
|
const onSync = (e: Event): void => {
|
||||||
|
const x = (e as CustomEvent<SyncDetail>).detail;
|
||||||
|
if (!root.isConnected) return document.removeEventListener(SK_SYNC, onSync);
|
||||||
|
if (x.dk !== dk || x.from === me || !got) return;
|
||||||
|
if (x.got) {
|
||||||
|
got = x.got;
|
||||||
|
fails = [];
|
||||||
|
}
|
||||||
|
draw();
|
||||||
|
};
|
||||||
|
document.addEventListener(SK_SYNC, onSync);
|
||||||
|
|
||||||
const sheet = (행: number): void =>
|
const sheet = (행: number): void =>
|
||||||
void import("./M02_MasterTemplete_Hopyo_Sheet")
|
void import("./M02_MasterTemplete_Hopyo_Sheet")
|
||||||
@@ -114,6 +172,8 @@ export function sangeunStep(
|
|||||||
save({ ...d, 확인: d.확인.filter((n) => !made.has(n)) });
|
save({ ...d, 확인: d.확인.filter((n) => !made.has(n)) });
|
||||||
got = next;
|
got = next;
|
||||||
fails = next.못만듦;
|
fails = next.못만듦;
|
||||||
|
tell({ got: next });
|
||||||
|
if (made.size) document.dispatchEvent(new Event(HOPYO_BUILT)); // 열린 엑셀 보기 칠 · 좌측 나무 다시
|
||||||
showToast(
|
showToast(
|
||||||
L("M02_SkMoved")
|
L("M02_SkMoved")
|
||||||
.replace("{made}", String(made.size))
|
.replace("{made}", String(made.size))
|
||||||
@@ -129,18 +189,39 @@ export function sangeunStep(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const cols: Column[] = [
|
const cols: Column[] = o.narrow
|
||||||
{ label: L("M02_SkColBlock"), width: "72px" },
|
? [
|
||||||
{ label: L("M02_SangeunColName") },
|
{ label: L("M02_SkColBlock") },
|
||||||
{ label: L("M02_SangeunColUnit"), width: "56px" },
|
{ label: L("M02_SkColCall"), width: "92px" },
|
||||||
{ label: L("M02_SkColVerdict") },
|
{ label: L("M02_SkColVerdict"), width: "92px" },
|
||||||
{ label: L("M02_SkColOurs"), align: "end" },
|
{ label: L("M02_SkColMade"), width: "92px" },
|
||||||
{ label: L("M02_SkColExcel"), align: "end" },
|
{ label: L("M02_SkSure"), width: "64px" },
|
||||||
{ label: L("M02_SkColBasis") },
|
{ label: "", width: "60px" },
|
||||||
{ label: L("M02_SkColMade"), width: "110px" },
|
]
|
||||||
{ label: L("M02_SkSure"), width: "72px" },
|
: [
|
||||||
{ label: "", width: "96px" },
|
{ label: L("M02_SkColBlock"), width: "72px" },
|
||||||
];
|
{ label: L("M02_SangeunColName") },
|
||||||
|
{ label: L("M02_SangeunColUnit"), width: "56px" },
|
||||||
|
{ label: L("M02_SkColCall"), width: "104px" },
|
||||||
|
{ label: L("M02_SkColVerdict") },
|
||||||
|
{ label: L("M02_SkColOurs"), align: "end" },
|
||||||
|
{ label: L("M02_SkColExcel"), align: "end" },
|
||||||
|
{ label: L("M02_SkColBasis") },
|
||||||
|
{ label: L("M02_SkColMade"), width: "110px" },
|
||||||
|
{ label: L("M02_SkSure"), width: "72px" },
|
||||||
|
{ label: "", width: "96px" },
|
||||||
|
];
|
||||||
|
|
||||||
|
/** 부름 칸 — 「일위대가 n 곳」(title 호표 번) · 「일위대가 밖」 · 옛 서버(칸 없음) = — */
|
||||||
|
function callCell(b: SkBlock): HTMLElement | string {
|
||||||
|
if (!b.부름) return "—";
|
||||||
|
const out = outside(b);
|
||||||
|
const word = out ? L("M02_SkCallOut") : L("M02_SkCallIn").replace("{n}", String(b.부름.length)); // prettier-ignore
|
||||||
|
const badge = createBadge(word, out ? "neutral" : "info");
|
||||||
|
badge.title = b.부름.map((n) => `제${n}호표`).join(" · ");
|
||||||
|
badge.dataset.skCall = out ? "out" : "in";
|
||||||
|
return badge;
|
||||||
|
}
|
||||||
|
|
||||||
/** 블록 한 줄 + 펼친 줄 표 */
|
/** 블록 한 줄 + 펼친 줄 표 */
|
||||||
function blockRows(b: SkBlock, d: SkDraft): HTMLTableRowElement[] {
|
function blockRows(b: SkBlock, d: SkDraft): HTMLTableRowElement[] {
|
||||||
@@ -161,10 +242,13 @@ export function sangeunStep(
|
|||||||
onChange: (on) => {
|
onChange: (on) => {
|
||||||
save(setSure(load(), b, on));
|
save(setSure(load(), b, on));
|
||||||
draw();
|
draw();
|
||||||
|
tell();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
sure.root.dataset.skSure = String(b.번);
|
sure.root.dataset.skSure = String(b.번);
|
||||||
sure.root.title = ok ? L("M02_SkSureTip") : L("M02_SkSureOff");
|
sure.root.title = ok
|
||||||
|
? L("M02_SkSureTip")
|
||||||
|
: L(b.줄.length ? "M02_SkSureOff" : "M02_SkSureEmpty");
|
||||||
const lines = createButton({
|
const lines = createButton({
|
||||||
label: `${L("M02_SkOpen")} ${b.줄.length}`,
|
label: `${L("M02_SkOpen")} ${b.줄.length}`,
|
||||||
size: 24,
|
size: 24,
|
||||||
@@ -177,20 +261,34 @@ export function sangeunStep(
|
|||||||
});
|
});
|
||||||
lines.dataset.skOpen = String(b.번);
|
lines.dataset.skOpen = String(b.번);
|
||||||
const first = b.줄.find((l) => l.행 !== null)?.행 ?? null;
|
const first = b.줄.find((l) => l.행 !== null)?.행 ?? null;
|
||||||
const view = first !== null ? createButton({ label: L("M02_SkSheet"), size: 24, variant: "link", onClick: () => sheet(first) }) : ""; // prettier-ignore
|
const view = first !== null && !o.narrow ? createButton({ label: L("M02_SkSheet"), size: 24, variant: "link", onClick: () => sheet(first) }) : ""; // prettier-ignore
|
||||||
const head = tableRow(cols, [
|
const name = `단산${b.번} ${b.이름 ?? ""}${b.단위 ? ` [${b.단위}]` : ""}`;
|
||||||
`단산${b.번}`,
|
const head = tableRow(
|
||||||
b.이름 ?? "",
|
cols,
|
||||||
b.단위 ?? "",
|
o.narrow
|
||||||
verdict,
|
? [name, callCell(b), verdict, made, sure.root, lines]
|
||||||
won(b.우리금액),
|
: [
|
||||||
won(b.엑셀금액),
|
`단산${b.번}`,
|
||||||
basis,
|
b.이름 ?? "",
|
||||||
made,
|
b.단위 ?? "",
|
||||||
sure.root,
|
callCell(b),
|
||||||
el("div", { className: "ui-toolbar", children: [lines, view] }),
|
verdict,
|
||||||
]);
|
won(b.우리금액),
|
||||||
|
won(b.엑셀금액),
|
||||||
|
basis,
|
||||||
|
made,
|
||||||
|
sure.root,
|
||||||
|
el("div", { className: "ui-toolbar", children: [lines, view] }),
|
||||||
|
],
|
||||||
|
);
|
||||||
head.dataset.skBlock = String(b.번);
|
head.dataset.skBlock = String(b.번);
|
||||||
|
if (b.번 === current) head.classList.add("is-current");
|
||||||
|
if (o.narrow) {
|
||||||
|
head.title = `${name} · ${L("M02_SkColOurs")} ${won(b.우리금액)} · ${L("M02_SkColExcel")} ${won(b.엑셀금액)}`; // prettier-ignore
|
||||||
|
head.addEventListener("click", (e) => {
|
||||||
|
if (!(e.target as Element).closest("button, label, input")) o.onRow?.(b.번);
|
||||||
|
});
|
||||||
|
}
|
||||||
if (!open.has(b.번)) return [head];
|
if (!open.has(b.번)) return [head];
|
||||||
const table = skLineTable({
|
const table = skLineTable({
|
||||||
scope,
|
scope,
|
||||||
@@ -199,6 +297,7 @@ export function sangeunStep(
|
|||||||
onPick: (at, p) => {
|
onPick: (at, p) => {
|
||||||
save(tidy(setPick(load(), b.번, at, p), got!.판정));
|
save(tidy(setPick(load(), b.번, at, p), got!.판정));
|
||||||
draw();
|
draw();
|
||||||
|
tell();
|
||||||
},
|
},
|
||||||
onSheet: sheet,
|
onSheet: sheet,
|
||||||
});
|
});
|
||||||
@@ -221,7 +320,8 @@ export function sangeunStep(
|
|||||||
.replace("{made}", String(made))
|
.replace("{made}", String(made))
|
||||||
.replace("{sure}", String(d.확인.length));
|
.replace("{sure}", String(d.확인.length));
|
||||||
const { root: table, tbody } = tableShell(cols, { density: "data" });
|
const { root: table, tbody } = tableShell(cols, { density: "data" });
|
||||||
for (const b of blocks) tbody.append(...blockRows(b, d));
|
const shown = filterBlocks(blocks, d, filter);
|
||||||
|
for (const b of shown) tbody.append(...blockRows(b, d));
|
||||||
const band = fails.length
|
const band = fails.length
|
||||||
? [
|
? [
|
||||||
createBanner({
|
createBanner({
|
||||||
@@ -231,10 +331,12 @@ export function sangeunStep(
|
|||||||
}),
|
}),
|
||||||
]
|
]
|
||||||
: [];
|
: [];
|
||||||
|
const far = blocks.filter(outside).length;
|
||||||
|
const where = blocks.some((b) => b.부름) ? ` · ${L("M02_SkSummaryIn").replace("{in}", String(blocks.length - far)).replace("{out}", String(far))}` : ""; // prettier-ignore
|
||||||
out.replaceChildren(
|
out.replaceChildren(
|
||||||
createText(sum, "muted", 14),
|
createText(sum + where, "muted", 14),
|
||||||
...band,
|
...band,
|
||||||
blocks.length ? table : createText(L("M02_SkNone"), "muted", 14),
|
shown.length ? table : createText(L("M02_SkNone"), "muted", 14),
|
||||||
);
|
);
|
||||||
move.disabled = !d.확인.length;
|
move.disabled = !d.확인.length;
|
||||||
move.querySelector(".ui-btn__label")!.textContent = L("M02_SkMove").replace(
|
move.querySelector(".ui-btn__label")!.textContent = L("M02_SkMove").replace(
|
||||||
@@ -253,5 +355,18 @@ export function sangeunStep(
|
|||||||
draw();
|
draw();
|
||||||
out.querySelector(`[data-sk-block="${번}"]`)?.scrollIntoView({ block: "center" });
|
out.querySelector(`[data-sk-block="${번}"]`)?.scrollIntoView({ block: "center" });
|
||||||
},
|
},
|
||||||
|
mark: (번) => {
|
||||||
|
out.querySelector("tr.is-current")?.classList.remove("is-current");
|
||||||
|
current = 번;
|
||||||
|
if (번 === null || !got) return;
|
||||||
|
if (!out.querySelector(`[data-sk-block="${번}"]`) && filter !== "all") {
|
||||||
|
filter = "all";
|
||||||
|
seg.set("all");
|
||||||
|
draw();
|
||||||
|
}
|
||||||
|
const row = out.querySelector(`[data-sk-block="${번}"]`);
|
||||||
|
row?.classList.add("is-current");
|
||||||
|
row?.scrollIntoView({ block: "nearest" });
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ export function lineTone(
|
|||||||
export const worst = (tones: Tone[]): Tone =>
|
export const worst = (tones: Tone[]): Tone =>
|
||||||
tones.reduce<Tone>((a, b) => (RANK[b] > RANK[a] ? b : a), "muted");
|
tones.reduce<Tone>((a, b) => (RANK[b] > RANK[a] ? b : a), "muted");
|
||||||
|
|
||||||
/** 블록 칠 — 만든 SK 갈래 › 없으면 부르는 첫 줄 판정 「예상」 · 판정 없음 = 밖 */
|
/** 블록 칠 — 만든 SK 갈래 › 없으면 부르는 첫 줄 판정 「예상」 · 판정 없음 = 「일위대가 밖」 */
|
||||||
export function blockTone(m: BlockMark | undefined): { tone: Tone; 글: string } {
|
export function blockTone(m: BlockMark | undefined): { tone: Tone; 글: string } {
|
||||||
if (!m) return { tone: "muted", 글: "안 불림" };
|
if (!m) return { tone: "muted", 글: "안 불림" };
|
||||||
if (m.SK) {
|
if (m.SK) {
|
||||||
@@ -123,7 +123,7 @@ export function blockTone(m: BlockMark | undefined): { tone: Tone; 글: string }
|
|||||||
const tone: Tone = lost ? "danger" : m.갈래 === "SK" ? "warning" : "success";
|
const tone: Tone = lost ? "danger" : m.갈래 === "SK" ? "warning" : "success";
|
||||||
return { tone, 글: `${m.SK} · ${m.갈래 ?? ""}${lost ? ` · 못옮김 ${lost}` : ""}` };
|
return { tone, 글: `${m.SK} · ${m.갈래 ?? ""}${lost ? ` · 못옮김 ${lost}` : ""}` };
|
||||||
}
|
}
|
||||||
if (!m.판정) return { tone: "muted", 글: "판정 없음" };
|
if (!m.판정) return { tone: "muted", 글: "일위대가 밖" }; // 부르는 일위대가 줄 없음(780-2)
|
||||||
const tone: Tone = m.판정 === "맞음" ? "success" : m.판정 === "다른 점" ? "warning" : "info";
|
const tone: Tone = m.판정 === "맞음" ? "success" : m.판정 === "다른 점" ? "warning" : "info";
|
||||||
return { tone, 글: `예상 — 부르는 첫 줄 ${m.판정}` };
|
return { tone, 글: `예상 — 부르는 첫 줄 ${m.판정}` };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
* · 고름 캐시 `{고름 {번: {차례: 고름}}, 확인 [번]}` — 줄 고름이 서버 상태를 덮음(꼬리표 「내가 고름」)
|
* · 고름 캐시 `{고름 {번: {차례: 고름}}, 확인 [번]}` — 줄 고름이 서버 상태를 덮음(꼬리표 「내가 고름」)
|
||||||
* · 블록 [확인] = 남은 고를 줄 0 일 때만 · 몸 = `{확인: [{번, 고름?}]}`(확인한 블록만 · 자동 확인 없음)
|
* · 블록 [확인] = 남은 고를 줄 0 일 때만 · 몸 = `{확인: [{번, 고름?}]}`(확인한 블록만 · 자동 확인 없음)
|
||||||
* · 금액 · 판정 정본은 서버 — 여기는 캐시를 겹쳐 보이기 · 몸 짓기만
|
* · 금액 · 판정 정본은 서버 — 여기는 캐시를 겹쳐 보이기 · 몸 짓기만
|
||||||
|
* · 780-2(요구 627) — 블록 `부름 [호표 번]`(빈 = 일위대가 밖 · 칸 없음 = 옛 서버 = 안) · 줄 0 = 「줄 없음」 확인 막음 ·
|
||||||
|
* 거르기(모두 · 안 · 밖 · 고를 줄 있음)
|
||||||
* 화면 · fetch · sessionStorage 없음(`CLAUDE.md` 4장).
|
* 화면 · fetch · sessionStorage 없음(`CLAUDE.md` 4장).
|
||||||
* ========================================================================== */
|
* ========================================================================== */
|
||||||
|
|
||||||
@@ -65,6 +67,8 @@ export interface SkBlock {
|
|||||||
근거: { 로직: string; 이름: string | null; 판정: string | null } | null;
|
근거: { 로직: string; 이름: string | null; 판정: string | null } | null;
|
||||||
만든: string | null;
|
만든: string | null;
|
||||||
줄: SkLine[];
|
줄: SkLine[];
|
||||||
|
/** 부르는 일위대가 호표 번(780-1) — [] = 일위대가 밖 · 없음 = 옛 서버(안) */
|
||||||
|
부름?: number[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SkJudged {
|
export interface SkJudged {
|
||||||
@@ -91,6 +95,7 @@ export interface SkDraft {
|
|||||||
|
|
||||||
export const MINE = "내가 고름";
|
export const MINE = "내가 고름";
|
||||||
export const GOOD = "옮길 수 있음";
|
export const GOOD = "옮길 수 있음";
|
||||||
|
export const NO_LINES = "줄 없음";
|
||||||
export const LINKED = "이어짐";
|
export const LINKED = "이어짐";
|
||||||
export const PICK = "고를 것";
|
export const PICK = "고를 것";
|
||||||
export const OUT = "밖";
|
export const OUT = "밖";
|
||||||
@@ -143,16 +148,17 @@ export function lineView(
|
|||||||
/** 그 블록 캐시 고름 `{차례: 고름}` */
|
/** 그 블록 캐시 고름 `{차례: 고름}` */
|
||||||
export const picksOf = (d: SkDraft, 번: number): Record<string, SkPick> => d.고름[String(번)] ?? {};
|
export const picksOf = (d: SkDraft, 번: number): Record<string, SkPick> => d.고름[String(번)] ?? {};
|
||||||
|
|
||||||
/** 남은 고를 줄 수 — 이어짐 · 밖 · 뺌 아닌 줄(캐시 고름 겹침) */
|
/** 남은 고를 줄 수 — 이어짐 · 밖 · 뺌 아닌 줄(캐시 고름 겹침) · 줄 0 블록 = 1(서버 「줄 없음」 고를줄 1) */
|
||||||
export const leftLines = (b: SkBlock, picks: Record<string, SkPick>): number =>
|
export const leftLines = (b: SkBlock, picks: Record<string, SkPick>): number =>
|
||||||
b.줄.filter((l) => !DONE.has(lineView(l, picks[String(l.차례)]).상태)).length;
|
b.줄.length ? b.줄.filter((l) => !DONE.has(lineView(l, picks[String(l.차례)]).상태)).length : 1;
|
||||||
|
|
||||||
/** [확인] 켜짐 — 고를 줄 0 */
|
/** [확인] 켜짐 — 고를 줄 0 */
|
||||||
export const canConfirm = (b: SkBlock, picks: Record<string, SkPick>): boolean =>
|
export const canConfirm = (b: SkBlock, picks: Record<string, SkPick>): boolean =>
|
||||||
leftLines(b, picks) === 0;
|
leftLines(b, picks) === 0;
|
||||||
|
|
||||||
/** 블록 판정 글 — 캐시 겹친 뒤 「옮길 수 있음」 · 「고를 줄 n」 */
|
/** 블록 판정 글 — 캐시 겹친 뒤 「옮길 수 있음」 · 「고를 줄 n」 · 「줄 없음」 */
|
||||||
export function verdictOf(b: SkBlock, picks: Record<string, SkPick>): string {
|
export function verdictOf(b: SkBlock, picks: Record<string, SkPick>): string {
|
||||||
|
if (!b.줄.length) return NO_LINES;
|
||||||
const n = leftLines(b, picks);
|
const n = leftLines(b, picks);
|
||||||
return n ? `고를 줄 ${n}` : GOOD;
|
return n ? `고를 줄 ${n}` : GOOD;
|
||||||
}
|
}
|
||||||
@@ -201,10 +207,23 @@ export function sureBody(d: SkDraft, blocks: SkBlock[]): SkSure[] {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 마법사 [끝내기] 글 재료 — 아직 안 옮긴 블록 수 · 그 블록 고를 줄 합 */
|
/** 일위대가 밖 블록 — `부름` 빈 배열 */
|
||||||
export function waiting(d: SkDraft, blocks: SkBlock[]): { 블록: number; 줄: number } {
|
export const outside = (b: SkBlock): boolean => !!b.부름 && !b.부름.length;
|
||||||
|
|
||||||
|
/** 마법사 [끝내기] 글 재료 — 아직 안 옮긴 블록 수 · 그중 일위대가 밖 · 그 블록 고를 줄 합 */
|
||||||
|
export function waiting(d: SkDraft, blocks: SkBlock[]): { 블록: number; 밖: number; 줄: number } {
|
||||||
const open = blocks.filter((b) => !b.만든);
|
const open = blocks.filter((b) => !b.만든);
|
||||||
return { 블록: open.length, 줄: open.reduce((n, b) => n + leftLines(b, picksOf(d, b.번)), 0) };
|
const 줄 = open.reduce((n, b) => n + leftLines(b, picksOf(d, b.번)), 0);
|
||||||
|
return { 블록: open.length, 밖: open.filter(outside).length, 줄 };
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SkFilter = "all" | "in" | "out" | "open";
|
||||||
|
/** 거르기 — 모두 · 일위대가 안 · 일위대가 밖 · 고를 줄 있음(안 옮긴 블록 · 캐시 겹침) */
|
||||||
|
export function filterBlocks(blocks: SkBlock[], d: SkDraft, f: SkFilter): SkBlock[] {
|
||||||
|
if (f === "in") return blocks.filter((b) => !outside(b));
|
||||||
|
if (f === "out") return blocks.filter(outside);
|
||||||
|
if (f === "open") return blocks.filter((b) => !b.만든 && leftLines(b, picksOf(d, b.번)) > 0);
|
||||||
|
return blocks;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 줄 고름 넣기 · 지움(null) — 새 캐시 */
|
/** 줄 고름 넣기 · 지움(null) — 새 캐시 */
|
||||||
|
|||||||
@@ -1,11 +1,40 @@
|
|||||||
/* 778-5 D-5 마법사 단가산출 줄 고름 헬퍼 — `logic/Hopyo_SkPick`(순수) · `Hopyo_SkApi`(몸) 를 트랜스파일해 Node 로 돌림 ·
|
/* 778-5 D-5 마법사 단가산출 줄 고름 헬퍼 — `logic/Hopyo_SkPick`(순수) · `Hopyo_SkApi`(몸) 를 트랜스파일해 Node 로 돌림 ·
|
||||||
* fetch 대역(부른 길 · 몸 모음) · stdin = {blocks} · stdout = JSON. */
|
* fetch 대역(부른 길 · 몸 모음) · stdin = {blocks} · stdout = JSON.
|
||||||
|
* 780-2 — stdin `{sheetSk: {...}}` = 엑셀 보기 오른쪽 판 바꾸기(`Hopyo_SheetSk`) · 단가산출 칸 · 시트 막대 · 판은 가짜. */
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
const { makeLoader } = require("./helper_ts_stub_loader.cjs");
|
const { makeLoader } = require("./helper_ts_stub_loader.cjs");
|
||||||
|
|
||||||
const ROOT = path.join(__dirname, "..", "..");
|
const ROOT = path.join(__dirname, "..", "..");
|
||||||
const STUBS = { "@config/config_frontend": { API_BASE_URL: "/api", ROUTES: {} } };
|
const made = []; // 가짜 단가산출 칸 — 지은 인자 · 강조
|
||||||
|
const fakeEl = () => ({
|
||||||
|
hidden: false,
|
||||||
|
attrs: {},
|
||||||
|
kids: [],
|
||||||
|
setAttribute(k, v) {
|
||||||
|
this.attrs[k] = v;
|
||||||
|
},
|
||||||
|
append(x) {
|
||||||
|
this.kids.push(x);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const STUBS = {
|
||||||
|
"@config/config_frontend": { API_BASE_URL: "/api", ROUTES: {} },
|
||||||
|
"@ui/ui_template_elements": {
|
||||||
|
el: (tag, o = {}) => ({ ...fakeEl(), tag, className: o.className }),
|
||||||
|
},
|
||||||
|
"./M02_MasterTemplete_Hopyo_WizardSk": {
|
||||||
|
sangeunStep: (scope, key, file, onChange, o) => {
|
||||||
|
const step = {
|
||||||
|
args: { scope, key, file, o: { ...o, onRow: !!o.onRow } },
|
||||||
|
marks: [],
|
||||||
|
onRow: o.onRow,
|
||||||
|
};
|
||||||
|
made.push(step);
|
||||||
|
return { root: fakeEl(), mark: (n) => step.marks.push(n) };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
const stub = new Proxy({}, { get: () => () => {} });
|
const stub = new Proxy({}, { get: () => () => {} });
|
||||||
const { loadTs } = makeLoader({
|
const { loadTs } = makeLoader({
|
||||||
root: ROOT,
|
root: ROOT,
|
||||||
@@ -19,7 +48,9 @@ const { loadTs } = makeLoader({
|
|||||||
global.sessionStorage = { getItem: () => null, setItem: () => {}, removeItem: () => {} };
|
global.sessionStorage = { getItem: () => null, setItem: () => {}, removeItem: () => {} };
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
const { blocks } = JSON.parse(fs.readFileSync(0, "utf8"));
|
const input = JSON.parse(fs.readFileSync(0, "utf8"));
|
||||||
|
if (input.sheetSk) return process.stdout.write(JSON.stringify(sheetSk(input.sheetSk)));
|
||||||
|
const { blocks } = input;
|
||||||
const calls = [];
|
const calls = [];
|
||||||
global.fetch = async (url, init = {}) => {
|
global.fetch = async (url, init = {}) => {
|
||||||
calls.push({
|
calls.push({
|
||||||
@@ -77,8 +108,61 @@ global.sessionStorage = { getItem: () => null, setItem: () => {}, removeItem: ()
|
|||||||
await api.judgeSangeun({ layer: "personal", owner: null }, "HP-000001");
|
await api.judgeSangeun({ layer: "personal", owner: null }, "HP-000001");
|
||||||
await api.judgeSangeun({ layer: "personal", owner: null }, "HP-000001", out.body);
|
await api.judgeSangeun({ layer: "personal", owner: null }, "HP-000001", out.body);
|
||||||
out.calls = calls;
|
out.calls = calls;
|
||||||
|
// 780-2 일위대가 안 · 밖 · 줄 0 블록 — 거르기 · 판정 · 확인 막음 · 끝내기 글 재료
|
||||||
|
const more = [
|
||||||
|
{ ...a, 부름: [3, 7] },
|
||||||
|
{ ...b, 부름: [] },
|
||||||
|
{ ...b, 번: 9, 부름: [], 줄: [] },
|
||||||
|
{ ...c, 부름: [12], 만든: "SK-000001" },
|
||||||
|
];
|
||||||
|
const e = more[2];
|
||||||
|
let d2 = sk.setSure(sk.emptyDraft(), e, true);
|
||||||
|
out.empty = {
|
||||||
|
판정: sk.verdictOf(e, {}),
|
||||||
|
켜짐: sk.canConfirm(e, {}),
|
||||||
|
확인: d2.확인,
|
||||||
|
몸: sk.sureBody({ 고름: {}, 확인: [9] }, more),
|
||||||
|
};
|
||||||
|
d2 = sk.emptyDraft();
|
||||||
|
out.filter = Object.fromEntries(
|
||||||
|
["all", "in", "out", "open"].map((f) => [f, sk.filterBlocks(more, d2, f).map((x) => x.번)]),
|
||||||
|
);
|
||||||
|
out.old = sk.filterBlocks(blocks, d2, "out").length; // 옛 서버(부름 없음) = 모두 안
|
||||||
|
out.wait2 = sk.waiting(d2, more);
|
||||||
process.stdout.write(JSON.stringify(out));
|
process.stdout.write(JSON.stringify(out));
|
||||||
})().catch((error) => {
|
})().catch((error) => {
|
||||||
process.stderr.write(String(error?.stack ?? error));
|
process.stderr.write(String(error?.stack ?? error));
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/** 780-2 판 바꾸기 — 탭(역할) 차례로 고름 · 판 숨김 · 칸 지음 한 번 · 강조 · 줄 누름 뛰기 */
|
||||||
|
function sheetSk({ same, canWrite, picks }) {
|
||||||
|
const M02 = path.join(ROOT, "M02_MasterTemplete");
|
||||||
|
const { createSheetSk } = loadTs(path.join(M02, "M02_MasterTemplete_Hopyo_SheetSk.ts"));
|
||||||
|
const roles = { s1: "일위대가표", s2: "단가산출표", s3: "단가산출상세" };
|
||||||
|
const jumps = [];
|
||||||
|
const nav = {
|
||||||
|
role: (sheet) => roles[sheet],
|
||||||
|
blockAt: (sheet, row) =>
|
||||||
|
sheet === "s2" ? row - 4 : sheet === "s3" ? Math.floor(row / 10) : null,
|
||||||
|
spotOf: (no, role) => ({ 시트: role === "단가산출표" ? "s2" : "s3", 행: [no, no + 1] }),
|
||||||
|
jumpTo: (sheet, rows) => jumps.push([sheet, rows]),
|
||||||
|
};
|
||||||
|
const toggled = [];
|
||||||
|
const side = {
|
||||||
|
hidden: false,
|
||||||
|
after: () => {},
|
||||||
|
parentElement: { classList: { toggle: (c, on) => toggled.push([c, on]) } },
|
||||||
|
};
|
||||||
|
const scope = { layer: "personal", owner: null };
|
||||||
|
const target = same ? scope : { layer: "company", owner: null };
|
||||||
|
const p = createSheetSk({ scope, key: "HP-000001", 파일: "a.xls", target, canWrite, nav, side });
|
||||||
|
const seen = [];
|
||||||
|
for (const [sheet, cell] of picks) {
|
||||||
|
p.at({ 시트: sheet, 칸: cell });
|
||||||
|
seen.push({ 호표판숨김: side.hidden, 단가판숨김: p.root.hidden, 지음: made.length });
|
||||||
|
if (sheet === "s2" && made[0]) made[0].onRow(8); // 목록 줄 누름(단가산출표 탭)
|
||||||
|
if (sheet === "s3" && made[0]) made[0].onRow(8); // 상세 탭
|
||||||
|
}
|
||||||
|
return { seen, toggled, made: made.map((m) => ({ args: m.args, marks: m.marks })), jumps };
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
"""780-2 지킴 — 엑셀 보기 단가산출표 · 상세 탭 = 오른쪽 단가산출 목록(요구 627).
|
||||||
|
|
||||||
|
서버 없이 잡음: 탭(역할) 따라 판 바꿈(일위대가표 = 호표 목록 · 단가산출 탭 = 단가산출 목록 · 칸은 처음 한 번만 지음) ·
|
||||||
|
시트 행 → 목록 강조 · 목록 줄 → 지금 탭의 블록 자리 · 만들 층 ≠ 파일 층 = 보기만 · 거르기(전체 · 안 · 밖 · 고를 줄 있음) ·
|
||||||
|
줄 0 블록 = 「줄 없음」 확인 막음 · 마법사 칸과 같은 캐시 열쇠(두 벌 고름 맞춤) · [끝내기] = 막지 않고 물음창 글만 ·
|
||||||
|
화면 = 부품 상자만 · 700 줄.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
HERE = os.path.dirname(__file__)
|
||||||
|
M02 = Path(__file__).resolve().parents[2] / "M02_MasterTemplete"
|
||||||
|
from test_778_5_sk_pick_screen import BLOCKS as sys_blocks # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
|
def _node(payload: dict) -> dict:
|
||||||
|
proc = subprocess.run(
|
||||||
|
["node", os.path.join(HERE, "helper_778_5_sk_pick.cjs")],
|
||||||
|
input=json.dumps(payload, ensure_ascii=False),
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
encoding="utf-8",
|
||||||
|
check=False,
|
||||||
|
)
|
||||||
|
assert proc.returncode == 0, proc.stderr
|
||||||
|
return json.loads(proc.stdout)
|
||||||
|
|
||||||
|
|
||||||
|
PICKS = [["s1", "B5"], ["s2", "B12"], ["s1", "C3"], ["s3", "A85"], ["s3", "A2"]]
|
||||||
|
|
||||||
|
|
||||||
|
def test_판_바꿈_목록_지음_강조_뛰기():
|
||||||
|
got = _node({"sheetSk": {"same": True, "canWrite": True, "picks": PICKS}})
|
||||||
|
assert got["seen"] == [
|
||||||
|
{"호표판숨김": False, "단가판숨김": True, "지음": 0}, # 일위대가표 = 호표 목록 · 칸 안 지음
|
||||||
|
{"호표판숨김": True, "단가판숨김": False, "지음": 1}, # 단가산출표 = 단가산출 목록
|
||||||
|
{"호표판숨김": False, "단가판숨김": True, "지음": 1},
|
||||||
|
{"호표판숨김": True, "단가판숨김": False, "지음": 1}, # 다시 와도 한 번만 지음
|
||||||
|
{"호표판숨김": True, "단가판숨김": False, "지음": 1},
|
||||||
|
]
|
||||||
|
assert [on for _, on in got["toggled"]] == [False, True, False, True, True]
|
||||||
|
(step,) = got["made"]
|
||||||
|
assert step["args"]["o"] == {"narrow": True, "onRow": True, "readOnly": False}
|
||||||
|
assert step["args"]["key"] == "HP-000001"
|
||||||
|
assert step["marks"] == [8, 8, 0] # 목록 행 12 → 단산8 · 상세 85 행 → 단산8 · 2 행 → 블록 0
|
||||||
|
# 목록 줄 누름 = 지금 탭의 자리(단가산출표 = 목록 행 · 상세 = 블록 범위)
|
||||||
|
assert got["jumps"] == [["s2", [8, 9]], ["s3", [8, 9]], ["s3", [8, 9]]]
|
||||||
|
|
||||||
|
|
||||||
|
def test_다른_층_권한_없음_보기만():
|
||||||
|
for same, can in ((False, True), (True, False)):
|
||||||
|
got = _node({"sheetSk": {"same": same, "canWrite": can, "picks": [["s2", "B5"]]}})
|
||||||
|
assert got["made"][0]["args"]["o"]["readOnly"] is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_거르기_줄0_막음_끝내기_글():
|
||||||
|
got = _node({"blocks": sys_blocks})
|
||||||
|
assert got["empty"] == {"판정": "줄 없음", "켜짐": False, "확인": [], "몸": []}
|
||||||
|
assert got["filter"] == {"all": [2, 5, 9, 7], "in": [2, 7], "out": [5, 9], "open": [2, 9]}
|
||||||
|
assert got["old"] == 0 # 옛 서버(부름 없음) = 모두 일위대가 안
|
||||||
|
assert got["wait2"] == {"블록": 3, "밖": 2, "줄": 3} # 만든 블록 7 뺌 · 줄 0 블록 = 고를 줄 1
|
||||||
|
|
||||||
|
|
||||||
|
def _src(name: str) -> str:
|
||||||
|
return (M02 / f"M02_MasterTemplete_Hopyo_{name}.ts").read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def test_캐시_공유_알림_칠_다시():
|
||||||
|
wiz = _src("WizardSk")
|
||||||
|
assert wiz.count("skDraftKey(scope, key)") == 1 # 두 벌 모두 같은 열쇠(층 · 주인 · 파일 키)
|
||||||
|
assert "SK_SYNC" in wiz and "tell({ got: next })" in wiz # 옮김 = 받은 판정 그대로 다른 벌에
|
||||||
|
assert wiz.count("tell();") == 2 # [확인] · 줄 고름 바뀜
|
||||||
|
assert "new Event(HOPYO_BUILT)" in wiz # 옮김 뒤 열린 엑셀 보기 칠 다시
|
||||||
|
sheet = _src("Sheet")
|
||||||
|
assert "createSheetSk(" in sheet and "sk?.at(s);" in sheet
|
||||||
|
side = _src("SheetSk")
|
||||||
|
assert "sangeunStep(opt.scope, opt.key" in side # 마법사와 같은 파일 층 · 키
|
||||||
|
nav = _src("SheetNav")
|
||||||
|
assert "blockAt(sheet: string, row: number)" in nav and "spotOf(no: number, role: Role)" in nav
|
||||||
|
wizard = _src("Wizard")
|
||||||
|
assert 'L("M02_SkUnmoved")' in wizard and "finish.disabled = !!n?.줄;" in wizard # 밖 = 안 막음
|
||||||
|
tone = (M02 / "logic" / "M02_MasterTemplete_Hopyo_SheetMap.ts").read_text(encoding="utf-8")
|
||||||
|
assert '글: "일위대가 밖"' in tone
|
||||||
|
|
||||||
|
|
||||||
|
def test_부품_상자만_700줄():
|
||||||
|
for name in ("SheetSk", "Sheet", "SheetNav", "WizardSk", "Wizard"):
|
||||||
|
src = _src(name)
|
||||||
|
assert len(src.splitlines()) <= 700, name
|
||||||
|
assert not re.search(r"#[0-9a-fA-F]{3,6}\b|rgb\(", src), name
|
||||||
|
for name in ("SheetSk", "WizardSk"):
|
||||||
|
assert not re.search(r'el\("(button|input|select)"|<button|<input', _src(name)), name
|
||||||
|
css = (M02 / "M02_MasterTemplete_Hopyo.css").read_text(encoding="utf-8")
|
||||||
|
block = css.split(".m02-hopyo-sheet.is-sk", 1)[1].split("/*", 1)[0]
|
||||||
|
assert not re.search(r"#[0-9a-fA-F]{3,6}\b|rgb\(", block)
|
||||||
@@ -92,7 +92,11 @@ def test_몸_꼴_이음_줄_자동_확인_없음():
|
|||||||
assert first["method"] == "POST" and first["body"] == {"확인": []} # 판정만
|
assert first["method"] == "POST" and first["body"] == {"확인": []} # 판정만
|
||||||
assert first["url"].endswith("/layers/personal/hopyo/HP-000001/sangeun")
|
assert first["url"].endswith("/layers/personal/hopyo/HP-000001/sangeun")
|
||||||
assert second["body"] == {"확인": got["body"]}
|
assert second["body"] == {"확인": got["body"]}
|
||||||
assert got["wait"] == {"블록": 3, "줄": 1} # 안 옮긴 블록 셋 · 블록 7 하위 줄
|
assert got["wait"] == {
|
||||||
|
"블록": 3,
|
||||||
|
"밖": 0,
|
||||||
|
"줄": 1,
|
||||||
|
} # 안 옮긴 블록 셋 · 블록 7 하위 줄 · 옛 서버 = 밖 없음
|
||||||
assert got["tidy"] == [5] # 고름 지우면 그 블록 확인 빠짐
|
assert got["tidy"] == [5] # 고름 지우면 그 블록 확인 빠짐
|
||||||
assert got["round"]["확인"] == [2, 5] and got["bad"] == {"고름": {}, "확인": []}
|
assert got["round"]["확인"] == [2, 5] and got["bad"] == {"고름": {}, "확인": []}
|
||||||
|
|
||||||
|
|||||||
@@ -261,6 +261,21 @@ export const ui_locales_m2 = {
|
|||||||
M02_SkSheet: ["보기", "View"],
|
M02_SkSheet: ["보기", "View"],
|
||||||
M02_SkNone: ["단가산출 블록 없음", "No unit-cost blocks"],
|
M02_SkNone: ["단가산출 블록 없음", "No unit-cost blocks"],
|
||||||
M02_SkNoLines: ["줄 없음", "No lines"],
|
M02_SkNoLines: ["줄 없음", "No lines"],
|
||||||
|
// 780-2 단가산출 목록(엑셀 보기 오른쪽 · 요구 627)
|
||||||
|
M02_SkFilter: ["거르기", "Filter"],
|
||||||
|
M02_SkFilterAll: ["전체", "All"],
|
||||||
|
M02_SkFilterIn: ["일위대가 안", "In unit-price sheet"],
|
||||||
|
M02_SkFilterOut: ["일위대가 밖", "Outside unit-price sheet"],
|
||||||
|
M02_SkFilterOpen: ["고를 줄 있음", "Open lines"],
|
||||||
|
M02_SkColCall: ["부름", "Called by"],
|
||||||
|
M02_SkCallIn: ["일위대가 {n} 곳", "{n} unit-price rows"],
|
||||||
|
M02_SkCallOut: ["일위대가 밖", "Outside"],
|
||||||
|
M02_SkSummaryIn: ["일위대가 안 {in} · 밖 {out}", "in sheet {in} · outside {out}"],
|
||||||
|
M02_SkSureEmpty: ["줄 없음 — 옮길 것 없음", "No lines — nothing to move"],
|
||||||
|
M02_SkUnmoved: [
|
||||||
|
"안 옮긴 단가산출 {n}(일위대가 밖 {m})",
|
||||||
|
"Unit-cost blocks not moved {n} (outside {m})",
|
||||||
|
],
|
||||||
M02_SkBasisTag: ["근거(참고) —", "Basis (ref) —"],
|
M02_SkBasisTag: ["근거(참고) —", "Basis (ref) —"],
|
||||||
M02_SkLeft: [
|
M02_SkLeft: [
|
||||||
"단가산출 확인 안 한 블록 {n} · 고를 줄 {m}",
|
"단가산출 확인 안 한 블록 {n} · 고를 줄 {m}",
|
||||||
|
|||||||
Reference in New Issue
Block a user