- 새 M02_Library_Delete: 쓰는 줄 미리 보기(종류 · 이름 · 키 · 줄 · 층) · 권한 밖 409 막힘 목록 ·
확인 몸이면 쓰는 문서의 그 줄 지움(store.write 정본 다시 셈) 뒤 문서 지움 · _합친것/ 기록
(지운 문서 · 자리 · 고친 문서 앞판) · 되돌림(그새 고친 문서는 건너뜀) · 단가산출 바탕 · 더한줄 열쇠 당김
- 미리 보기 길 POST {kind}-tree/delete-check · undo 길이 지움 기록도 되돌림 · 합친 것 목록에 「지움」
- 나무 고침 몸 확인 칸 · 세션을 나무 고침에 넘김 · 옛 _refuse_used 걷음
- 화면: 지우기 앞 경고 창(쓰는 곳 N · 표 · 프로젝트 사본 영향 없음 · 되돌림 자리) · 부품 상자
확인창에 detail 칸(넓은 판 · 안 스크롤)
- 시험: 새 test_735_1 · test_716_4 의 쓰는 곳 409 시험을 확인 없음 409 + 목록 꼴로 고침
(지움이 막힘에서 경고 뒤 지움으로 바뀜)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
319 lines
12 KiB
TypeScript
319 lines
12 KiB
TypeScript
/* =============================================================================
|
|
* M02_MasterTemplete_Library_Same.ts
|
|
* 서고 [중복 찾기](733-3 · 요구서_572 3장 4) — 좌측 일위대가 · 단가산출 나무 메뉴에서 엶.
|
|
* 창 = 같은 내용 묶음(이 층 + 위 층 · 서버 `GET {kind}-same`) · 묶음마다 꼬리표(내용 같음 · 수량만 다름) ·
|
|
* 문서 줄(층 · 키 · 이름 · 구분 · 쓰는 곳 수) · [남김] 고르기 · [합치기](물음 → 서버 `merge` · 이 층 나머지 문서를
|
|
* 지움 · 쓰는 곳 참조를 남김으로) · 결과(합침 · 권한 밖 남김) · 합친 것 목록 [되돌림](`undo`) ·
|
|
* 나무 지우기 기록(735-1 · 남김 없음 = 「지움」 · 고친 쓰는 곳 수)도 같은 목록 · 같은 [되돌림].
|
|
* 권한 = 그 층 쓰기(서버가 봄) · 판 다르면 409 → 다시 찾음.
|
|
* ========================================================================== */
|
|
|
|
import { createBadge } from "@ui/ui_template_badge";
|
|
import { createButton, el, showToast } from "@ui/ui_template_elements";
|
|
import { createEmpty } from "@ui/ui_template_empty";
|
|
import { currentLanguageIndex } from "@ui/ui_template_locale";
|
|
import { askConfirm, openModal } from "@ui/ui_template_modal";
|
|
import { createTable, type Cell } from "@ui/ui_template_table";
|
|
import { createText } from "@ui/ui_template_text";
|
|
import { call, StaleError, type Layer, type LibraryKind } from "./M02_MasterTemplete_Api_Fetch";
|
|
import "./M02_MasterTemplete_Library_Same.css";
|
|
|
|
const TEXT = {
|
|
Title: ["중복 찾기", "Find duplicates"],
|
|
Close: ["닫기", "Close"],
|
|
Intro: [
|
|
"내용이 같은 문서 묶음 — [남김]을 고르고 [합치기]하면 이 층의 나머지 문서를 지우고 쓰는 곳을 남김으로 옮김 · [되돌림]으로 되살림",
|
|
"Groups with the same content — pick [Keep] and [Merge]: other docs on this layer are removed and their users point to the kept one · [Undo] restores",
|
|
],
|
|
None: ["같은 내용 문서 없음", "No duplicates"],
|
|
Loading: ["찾는 중…", "Searching…"],
|
|
Same: ["내용 같음", "Same content"],
|
|
QtyOnly: ["수량만 다름", "Quantity differs"],
|
|
Layer: ["층", "Layer"],
|
|
Key: ["키", "Key"],
|
|
Name: ["이름", "Name"],
|
|
Class: ["구분", "Category"],
|
|
Used: ["쓰는 곳", "Used by"],
|
|
Keep: ["남김", "Keep"],
|
|
Kept: ["남김 ✓", "Kept ✓"],
|
|
Merge: ["합치기", "Merge"],
|
|
Upper: ["위 층", "Upper"],
|
|
system: ["마스터", "Master"],
|
|
company: ["회사", "Company"],
|
|
personal: ["개인", "Personal"],
|
|
project: ["프로젝트", "Project"],
|
|
Ask: [
|
|
"「{keep}」 하나로 합침 — 이 층 문서 {n} 개를 지우고 쓰는 곳 참조를 옮김 · 지운 문서는 [되돌림]으로 되살림",
|
|
"Merge into “{keep}” — remove {n} doc(s) on this layer and move their users · [Undo] restores",
|
|
],
|
|
AskQty: [
|
|
"⚠ 실무 수량(엑셀 규격)이 서로 다름 — 지운 문서의 실무 수량 기록은 되돌림 때까지 안 보임",
|
|
"⚠ Practice quantities differ — removed docs' quantity record is hidden until undo",
|
|
],
|
|
Done: ["합침 {n} · 옮긴 쓰는 곳 {m}", "Merged {n} · moved users {m}"],
|
|
Left: [
|
|
"남김 — 「{key}」 쓰는 곳 {n}(권한 밖 · 못 옮김): {list}",
|
|
"Kept “{key}” — {n} user(s) not moved: {list}",
|
|
],
|
|
Gone: ["합친 것 · 지운 것(되돌림)", "Merged · deleted (undo)"],
|
|
GoneNone: ["합친 것 · 지운 것 없음", "Nothing merged or deleted"],
|
|
Deleted: ["지움", "Deleted"],
|
|
When: ["때", "When"],
|
|
Into: ["남김", "Kept as"],
|
|
Undo: ["되돌림", "Undo"],
|
|
AskUndo: [
|
|
"「{key}」 되돌림 — 문서를 되살리고 옮긴 쓰는 곳 {n} 을 다시 이 문서로",
|
|
"Undo “{key}” — restore it and point its {n} user(s) back",
|
|
],
|
|
AskUndoDeleted: [
|
|
"「{key}」 되돌림 — 지운 문서를 되살리고 쓰는 곳 {n} 에서 지운 줄을 다시 넣음(그새 고친 곳은 건너뜀)",
|
|
"Undo “{key}” — restore deleted doc(s) and put the lines back in {n} user(s) (skips ones edited since)",
|
|
],
|
|
Undone: ["되돌림 — 쓰는 곳 {n}", "Undone — users {n}"],
|
|
Stale: ["그새 바뀜 — 다시 찾음", "Changed meanwhile — searching again"],
|
|
} as const;
|
|
type TextKey = keyof typeof TEXT;
|
|
const tx = (key: TextKey, vars: Record<string, string | number> = {}): string =>
|
|
Object.entries(vars).reduce(
|
|
(s, [k, v]) => s.replace(`{${k}}`, String(v)),
|
|
String(TEXT[key][currentLanguageIndex as 0 | 1] ?? TEXT[key][0]),
|
|
);
|
|
|
|
/** 나무 메뉴 글 */
|
|
export const sameLabel = (): string => tx("Title");
|
|
|
|
export interface SameRef {
|
|
층: Layer;
|
|
주인: string | null;
|
|
키: string;
|
|
}
|
|
export interface SameDoc extends SameRef {
|
|
이름: string;
|
|
구분: string;
|
|
상세구분: string;
|
|
쓰는곳수: number;
|
|
위층: boolean;
|
|
}
|
|
export interface SameGroup {
|
|
지문: string;
|
|
꼬리표: "내용 같음" | "수량만 다름";
|
|
문서: SameDoc[];
|
|
}
|
|
export interface GoneRow {
|
|
키: string;
|
|
이름: string;
|
|
/** null = 나무 지우기 기록(735-1) */
|
|
남김: SameRef | null;
|
|
때: string;
|
|
옮긴곳수: number;
|
|
}
|
|
export interface SameView {
|
|
판: string;
|
|
묶음: SameGroup[];
|
|
합친것: GoneRow[];
|
|
}
|
|
export interface MergeResult extends SameView {
|
|
합침: { 키: string; 옮긴곳수: number }[];
|
|
남김: { 키: string; 쓰는곳: string[] }[];
|
|
}
|
|
|
|
const path = (kind: LibraryKind, layer: Layer, owner: string | null, tail = ""): string =>
|
|
`/layers/${layer}/${kind}-same${tail}${owner && layer !== "system" ? `?owner=${encodeURIComponent(owner)}` : ""}`;
|
|
const post = <T>(url: string, body: unknown): Promise<T> =>
|
|
call<T>(url, { method: "POST", body: JSON.stringify(body) });
|
|
const why = (error: unknown): string => (error instanceof Error ? error.message : "");
|
|
|
|
/** 묶음 하나에서 남김을 고를 때 지울 것 — 이 층(위 층 아님) 문서 가운데 남김 아닌 것 */
|
|
export const goneOf = (group: SameGroup, keep: SameRef): string[] =>
|
|
group.문서
|
|
.filter((d) => !d.위층 && !(d.층 === keep.층 && d.주인 === keep.주인 && d.키 === keep.키))
|
|
.map((d) => d.키);
|
|
|
|
export interface SameOptions {
|
|
kind: LibraryKind;
|
|
layer: Layer;
|
|
owner: string | null;
|
|
/** 합치기 · 되돌림 뒤 — 좌측 나무 다시 그림 */
|
|
changed: () => void;
|
|
}
|
|
|
|
export function openLibrarySame(opt: SameOptions): void {
|
|
const host = el("div", { className: "m02-same", attrs: { "data-library-same": opt.kind } });
|
|
const note = el("div", { className: "m02-same__note" });
|
|
let view: SameView | null = null;
|
|
|
|
const load = async (): Promise<void> => {
|
|
host.replaceChildren(createEmpty({ text: tx("Loading"), small: true }));
|
|
try {
|
|
view = await call<SameView>(path(opt.kind, opt.layer, opt.owner));
|
|
} catch (error) {
|
|
host.replaceChildren(createText(why(error), "error"));
|
|
return;
|
|
}
|
|
draw();
|
|
};
|
|
|
|
const run = async (job: () => Promise<SameView>, done: (got: SameView) => void) => {
|
|
try {
|
|
const got = await job();
|
|
view = got;
|
|
done(got);
|
|
opt.changed();
|
|
draw();
|
|
} catch (error) {
|
|
// 판 다름(409 · 서버 몸 = {stale, 판}) = 까닭 글 없음 → 「그새 바뀜」만
|
|
const text = why(error);
|
|
const stale = error instanceof StaleError;
|
|
showToast(stale && text.startsWith("HTTP") ? tx("Stale") : text, "error");
|
|
if (stale) void load();
|
|
}
|
|
};
|
|
|
|
const merge = async (group: SameGroup, keep: SameDoc): Promise<void> => {
|
|
const gone = goneOf(group, keep);
|
|
const message = [
|
|
tx("Ask", { keep: `${keep.키} ${keep.이름}`, n: gone.length }),
|
|
...(group.꼬리표 === "수량만 다름" ? [tx("AskQty")] : []),
|
|
].join("\n");
|
|
if (!view || !gone.length) return;
|
|
if (!(await askConfirm({ message, okLabel: tx("Merge"), title: tx("Title") }))) return;
|
|
const 판 = view.판;
|
|
await run(
|
|
() =>
|
|
post<MergeResult>(path(opt.kind, opt.layer, opt.owner, "/merge"), {
|
|
남김: { 층: keep.층, 주인: keep.주인, 키: keep.키 },
|
|
지움: gone,
|
|
판,
|
|
}),
|
|
(got) => {
|
|
const res = got as MergeResult;
|
|
const moved = res.합침.reduce((n, r) => n + r.옮긴곳수, 0);
|
|
showToast(tx("Done", { n: res.합침.length, m: moved }), "success");
|
|
note.replaceChildren(
|
|
...res.남김.map((k) =>
|
|
createText(
|
|
tx("Left", { key: k.키, n: k.쓰는곳.length, list: k.쓰는곳.join(" · ") }),
|
|
"warning",
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
};
|
|
|
|
const undo = async (row: GoneRow): Promise<void> => {
|
|
const ask = row.남김 ? "AskUndo" : "AskUndoDeleted";
|
|
const message = tx(ask, { key: `${row.키} ${row.이름}`, n: row.옮긴곳수 });
|
|
if (!(await askConfirm({ message, okLabel: tx("Undo"), title: tx("Title") }))) return;
|
|
await run(
|
|
() => post<SameView>(path(opt.kind, opt.layer, opt.owner, "/undo"), { 키: row.키 }),
|
|
(got) => {
|
|
const back = (got as SameView & { 되돌린곳수?: number }).되돌린곳수 ?? 0;
|
|
showToast(tx("Undone", { n: back }), "success");
|
|
note.replaceChildren();
|
|
},
|
|
);
|
|
};
|
|
|
|
const groupBlock = (group: SameGroup): HTMLElement => {
|
|
let keep = group.문서[0];
|
|
const columns = [
|
|
{ label: tx("Keep"), width: "88px" },
|
|
{ label: tx("Layer"), width: "88px" },
|
|
{ label: tx("Key"), width: "112px" },
|
|
{ label: tx("Name") },
|
|
{ label: tx("Class") },
|
|
{ label: tx("Used"), width: "72px", align: "end" as const },
|
|
];
|
|
const tableHost = el("div");
|
|
const paint = (): void => {
|
|
const rows: Cell[][] = group.문서.map((d) => [
|
|
createButton({
|
|
label: d === keep ? tx("Kept") : tx("Keep"),
|
|
variant: d === keep ? "filled" : "ghost",
|
|
size: "sm",
|
|
onClick: () => {
|
|
keep = d;
|
|
paint();
|
|
},
|
|
}),
|
|
d.위층 ? `${tx(d.층)} · ${tx("Upper")}` : tx(d.층),
|
|
d.키,
|
|
d.이름,
|
|
[d.구분, d.상세구분].filter(Boolean).join(" / "),
|
|
String(d.쓰는곳수),
|
|
]);
|
|
tableHost.replaceChildren(createTable(columns, rows, { density: "data" }));
|
|
mergeButton.disabled = !goneOf(group, keep).length;
|
|
};
|
|
const mergeButton = createButton({
|
|
label: tx("Merge"),
|
|
onClick: () => void merge(group, keep),
|
|
});
|
|
const tag = group.꼬리표 === "수량만 다름" ? tx("QtyOnly") : tx("Same");
|
|
const head = el("div", {
|
|
className: "m02-same__head",
|
|
children: [
|
|
createBadge(tag, group.꼬리표 === "수량만 다름" ? "warning" : "info"),
|
|
el("span", {
|
|
className: "m02-same__name",
|
|
text: `${group.문서[0].이름} · ${group.문서.length}`,
|
|
}),
|
|
mergeButton,
|
|
],
|
|
});
|
|
paint();
|
|
return el("section", {
|
|
className: "m02-same__group",
|
|
attrs: { "data-same-group": group.지문 },
|
|
children: [head, tableHost],
|
|
});
|
|
};
|
|
|
|
const goneBlock = (rows: GoneRow[]): HTMLElement => {
|
|
const columns = [
|
|
{ label: tx("Key"), width: "112px" },
|
|
{ label: tx("Name") },
|
|
{ label: tx("Into"), width: "140px" },
|
|
{ label: tx("When"), width: "160px" },
|
|
{ label: "", width: "88px" },
|
|
];
|
|
const cells: Cell[][] = rows.map((r) => [
|
|
r.키,
|
|
r.이름,
|
|
r.남김 ? `${tx(r.남김.층)} ${r.남김.키}` : tx("Deleted"),
|
|
r.때.replace("T", " "),
|
|
createButton({
|
|
label: tx("Undo"),
|
|
variant: "ghost",
|
|
size: "sm",
|
|
onClick: () => void undo(r),
|
|
}),
|
|
]);
|
|
return el("section", {
|
|
className: "m02-same__group",
|
|
attrs: { "data-same-gone": "" },
|
|
children: [
|
|
el("h4", { text: tx("Gone") }),
|
|
createTable(columns, cells, { density: "data", empty: tx("GoneNone") }),
|
|
],
|
|
});
|
|
};
|
|
|
|
const draw = (): void => {
|
|
if (!view) return;
|
|
const groups = view.묶음.length
|
|
? view.묶음.map(groupBlock)
|
|
: [createEmpty({ text: tx("None"), small: true })];
|
|
host.replaceChildren(createText(tx("Intro"), "muted"), note, ...groups, goneBlock(view.합친것));
|
|
};
|
|
|
|
openModal({
|
|
title: tx("Title"),
|
|
closeLabel: tx("Close"),
|
|
width: "960px",
|
|
mount: (body) => body.append(host),
|
|
});
|
|
void load();
|
|
}
|