Merge remote-tracking branch 'origin/sub_laptop_1' into sub_desktop_1
This commit is contained in:
@@ -58,28 +58,41 @@ export function valueWithUnit(value: unknown, unit?: string | null): string {
|
|||||||
|
|
||||||
/** 자료 출처표 한 줄 — 어디서 받고 최신이 무엇인지(데스크탑 서브 출처표 · 메인이 표 수준에 실음). */
|
/** 자료 출처표 한 줄 — 어디서 받고 최신이 무엇인지(데스크탑 서브 출처표 · 메인이 표 수준에 실음). */
|
||||||
export interface TableSource {
|
export interface TableSource {
|
||||||
|
source_id?: string;
|
||||||
|
/** 원본 이름 — 한 표가 원본 여럿을 모아 어느 것이 뒤처졌는지 가려야 함 */
|
||||||
|
name?: string;
|
||||||
publisher: string;
|
publisher: string;
|
||||||
where: string;
|
where: string;
|
||||||
cycle?: string;
|
cycle?: string;
|
||||||
latest_published?: string;
|
latest_published?: string;
|
||||||
our_edition?: string;
|
our_edition?: string;
|
||||||
checked_at?: string;
|
checked_at?: string;
|
||||||
/** 뒤처짐 판정 — 서버만 함 */
|
/** 뒤처짐 판정 — 서버만 함 · null 은 「모름」이라 띠를 안 세움 */
|
||||||
outdated?: boolean;
|
outdated?: boolean | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 뒤처진 자료 띠 글자 — 서버가 outdated 라 할 때만. 화면은 날짜를 견주지 않음(판정 두 벌 금지).
|
* 뒤처진 자료 띠 글자 — 서버가 outdated 라 할 때만. 화면은 날짜를 견주지 않음(판정 두 벌 금지).
|
||||||
* 받는 자리를 꼭 적음 — 최신이 무엇인지·어디서 받는지가 없어 건설 노임이 반 년 뒤처졌음.
|
* 받는 자리를 꼭 적음 — 최신이 무엇인지·어디서 받는지가 없어 건설 노임이 반 년 뒤처졌음.
|
||||||
*/
|
*/
|
||||||
export function outdatedBanner(source?: TableSource): string | null {
|
export function outdatedBanner(sources?: TableSource[]): string[] {
|
||||||
if (source?.outdated !== true) return null;
|
return (sources ?? [])
|
||||||
return `이 자료는 ${source.our_edition ?? "판 모름"} 판 · 최신은 ${source.latest_published ?? "?"} 공표 · ${source.publisher} ${source.where}`;
|
.filter((source) => source.outdated === true)
|
||||||
|
.map((source) => {
|
||||||
|
const head = source.name ? `${source.name} — ` : "";
|
||||||
|
const where = source.where ? ` ${source.where}` : "";
|
||||||
|
return `${head}이 자료는 ${source.our_edition ?? "판 모름"} 판 · 최신은 ${source.latest_published ?? "?"} 공표 · ${source.publisher}${where}`;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 기초단가 표 머리 — 열 하나에 한 문장이라 표에 한 번만 옴(2026-09-15 브레인 ④). */
|
/** 기초단가 표 머리 — 열 하나에 한 문장이라 표에 한 번만 옴(2026-09-15 브레인 ④). */
|
||||||
export interface TableMeta {
|
export interface TableMeta {
|
||||||
source?: TableSource;
|
/** 한 표가 모은 원본마다 한 줄 */
|
||||||
|
source?: TableSource[];
|
||||||
|
/** 서버가 주는 알림 한 줄(요율 — 법이 정한 값) — 화면이 사전을 가지면 두 벌이 됨 */
|
||||||
|
notice?: string;
|
||||||
|
/** 고친 값이 어디에 닿는지 — 새로 만드는 프로젝트부터 쓰임(서버가 줌) */
|
||||||
|
scope_notice?: string;
|
||||||
editable?: string[];
|
editable?: string[];
|
||||||
/** 열key → 왜 못 고치나 */
|
/** 열key → 왜 못 고치나 */
|
||||||
locked?: Record<string, string>;
|
locked?: Record<string, string>;
|
||||||
|
|||||||
@@ -33,8 +33,6 @@ type Row = Record<string, unknown>;
|
|||||||
export interface RowsSource {
|
export interface RowsSource {
|
||||||
title: string;
|
title: string;
|
||||||
key: string;
|
key: string;
|
||||||
/** 표 위 알림 한 줄(요율 — 법이 정한 값) */
|
|
||||||
notice?: string;
|
|
||||||
/** false 면 검색 칸을 숨김 — 서버에 검색이 없는 표(고친 것 모아 보기) */
|
/** false 면 검색 칸을 숨김 — 서버에 검색이 없는 표(고친 것 모아 보기) */
|
||||||
searchable?: boolean;
|
searchable?: boolean;
|
||||||
load: (query: RowsQuery) => Promise<MasterRows>;
|
load: (query: RowsQuery) => Promise<MasterRows>;
|
||||||
@@ -100,7 +98,8 @@ export function buildRowsView(): RowsView {
|
|||||||
children: [hiddenToggle, L("Z01_MasterData_ShowHidden")],
|
children: [hiddenToggle, L("Z01_MasterData_ShowHidden")],
|
||||||
});
|
});
|
||||||
const notice = el("p", { className: "z01-master__notice" });
|
const notice = el("p", { className: "z01-master__notice" });
|
||||||
const banner = el("p", { className: "z01-master__outdated" });
|
const scope = el("p", { className: "z01-master__scope" });
|
||||||
|
const banner = el("div", { className: "z01-master__outdated" });
|
||||||
const detail = el("div", { className: "z01-master__detail" });
|
const detail = el("div", { className: "z01-master__detail" });
|
||||||
const grid = el("div", { className: "z01-master__grid-wrap" });
|
const grid = el("div", { className: "z01-master__grid-wrap" });
|
||||||
const pageInput = el("input", {
|
const pageInput = el("input", {
|
||||||
@@ -128,6 +127,7 @@ export function buildRowsView(): RowsView {
|
|||||||
className: "z01-master__panel",
|
className: "z01-master__panel",
|
||||||
children: [
|
children: [
|
||||||
el("div", { children: [title, titleKey] }),
|
el("div", { children: [title, titleKey] }),
|
||||||
|
scope,
|
||||||
banner,
|
banner,
|
||||||
notice,
|
notice,
|
||||||
toolbar,
|
toolbar,
|
||||||
@@ -141,6 +141,7 @@ export function buildRowsView(): RowsView {
|
|||||||
detail.hidden = true;
|
detail.hidden = true;
|
||||||
notice.hidden = true;
|
notice.hidden = true;
|
||||||
banner.hidden = true;
|
banner.hidden = true;
|
||||||
|
scope.hidden = true;
|
||||||
|
|
||||||
function goTo(page: number): void {
|
function goTo(page: number): void {
|
||||||
const pages = pageCount(last?.total ?? 0, PAGE_SIZE);
|
const pages = pageCount(last?.total ?? 0, PAGE_SIZE);
|
||||||
@@ -190,10 +191,15 @@ export function buildRowsView(): RowsView {
|
|||||||
function draw(): void {
|
function draw(): void {
|
||||||
if (!last) return;
|
if (!last) return;
|
||||||
const meta = last;
|
const meta = last;
|
||||||
// 뒤처진 자료 띠 — 서버 outdated 만 봄(날짜 비교 안 함).
|
// 표 머리 세 줄 — 모두 서버가 주는 글자 그대로(화면이 사전을 가지면 두 벌이 됨).
|
||||||
|
// ① 고친 값이 닿는 자리 ② 뒤처진 원본(서버 outdated 만 · 날짜 비교 안 함) ③ 그 밖 알림
|
||||||
|
scope.textContent = meta.scope_notice ?? "";
|
||||||
|
scope.hidden = !meta.scope_notice;
|
||||||
const outdated = outdatedBanner(meta.source);
|
const outdated = outdatedBanner(meta.source);
|
||||||
banner.textContent = outdated ? `⚠ ${outdated}` : "";
|
banner.replaceChildren(...outdated.map((text) => el("p", { text: `⚠ ${text}` })));
|
||||||
banner.hidden = !outdated;
|
banner.hidden = outdated.length === 0;
|
||||||
|
notice.textContent = meta.notice ?? "";
|
||||||
|
notice.hidden = !meta.notice;
|
||||||
const columns = shownColumns(meta.columns, showHidden);
|
const columns = shownColumns(meta.columns, showHidden);
|
||||||
const headRow = el("tr", { children: columns.map((column) => headCell(meta, column)) });
|
const headRow = el("tr", { children: columns.map((column) => headCell(meta, column)) });
|
||||||
const body = el("tbody");
|
const body = el("tbody");
|
||||||
@@ -358,9 +364,9 @@ export function buildRowsView(): RowsView {
|
|||||||
state.page = 1;
|
state.page = 1;
|
||||||
title.textContent = picked.title;
|
title.textContent = picked.title;
|
||||||
titleKey.textContent = picked.key;
|
titleKey.textContent = picked.key;
|
||||||
notice.textContent = picked.notice ?? "";
|
notice.hidden = true;
|
||||||
notice.hidden = !picked.notice;
|
|
||||||
banner.hidden = true;
|
banner.hidden = true;
|
||||||
|
scope.hidden = true;
|
||||||
search.root.hidden = picked.searchable === false;
|
search.root.hidden = picked.searchable === false;
|
||||||
toolbar.hidden = false;
|
toolbar.hidden = false;
|
||||||
pager.hidden = false;
|
pager.hidden = false;
|
||||||
|
|||||||
@@ -44,8 +44,6 @@ export function buildSidePanel(groups: MasterGroup[], view: RowsView): HTMLEleme
|
|||||||
view.show({
|
view.show({
|
||||||
title: `${baseTitle} › ${label}`,
|
title: `${baseTitle} › ${label}`,
|
||||||
key: `base-prices/${kind}`,
|
key: `base-prices/${kind}`,
|
||||||
// 요율은 구간 줄에 코드가 없어 구간 이름으로 이음 — 법이 바뀌면 고친 값이 주인을 잃기 쉬움.
|
|
||||||
notice: kind === "rate" ? L("Z01_MasterData_Rate_Notice") : undefined,
|
|
||||||
load: (query) => fetchBasePrices(kind, query),
|
load: (query) => fetchBasePrices(kind, query),
|
||||||
save: (rowId, values) => saveBasePrice(kind, rowId, values),
|
save: (rowId, values) => saveBasePrice(kind, rowId, values),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -125,7 +125,22 @@
|
|||||||
font-size: var(--text-body);
|
font-size: var(--text-body);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 뒤처진 자료 띠 — 받는 자리까지 적힘 · 또렷이 */
|
/* 고친 값이 닿는 자리 — 흐리면 「저장이 안 됐다」로 읽힘(2026-09-16 브레인) */
|
||||||
|
.z01-master__scope {
|
||||||
|
margin: 0;
|
||||||
|
padding: var(--spacing-8) var(--spacing-12);
|
||||||
|
border-left: 4px solid var(--color-accent);
|
||||||
|
background: var(--color-mist-violet);
|
||||||
|
color: var(--color-accent);
|
||||||
|
font-size: var(--text-body-sm);
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 뒤처진 자료 띠 — 받는 자리까지 적힘 · 또렷이 · 원본마다 한 줄 */
|
||||||
|
.z01-master__outdated p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.z01-master__outdated {
|
.z01-master__outdated {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: var(--spacing-8) var(--spacing-12);
|
padding: var(--spacing-8) var(--spacing-12);
|
||||||
|
|||||||
@@ -29,18 +29,32 @@ import {
|
|||||||
} from "./Z01_MasterData_UI_Cells.js";
|
} from "./Z01_MasterData_UI_Cells.js";
|
||||||
|
|
||||||
// 뒤처짐 띠 — 판정은 서버 outdated 만(화면이 날짜를 안 견줌) · 받는 자리를 띠에 적음(2026-09-15 브레인)
|
// 뒤처짐 띠 — 판정은 서버 outdated 만(화면이 날짜를 안 견줌) · 받는 자리를 띠에 적음(2026-09-15 브레인)
|
||||||
const laborSource = {
|
// ⚠ 한 표가 원본 여럿을 모은 것이라 출처는 목록으로 옴(노임 = 건설업 + 제조업).
|
||||||
publisher: "대한건설협회",
|
const laborSources = [
|
||||||
where: "cak.or.kr [지원·사업]>[건설적산기준]>[건설임금]",
|
{
|
||||||
cycle: "반년",
|
source_id: "labor_const",
|
||||||
latest_published: "2026-09-01",
|
name: "건설업 시중노임단가",
|
||||||
our_edition: "2026-01-01",
|
publisher: "대한건설협회",
|
||||||
checked_at: "2026-09-15",
|
where: "cak.or.kr [지원·사업] > [건설적산기준] > [건설임금]",
|
||||||
outdated: true,
|
cycle: "해마다 두 번",
|
||||||
};
|
latest_published: "2026-09-01",
|
||||||
|
our_edition: "2026-01-01",
|
||||||
|
checked_at: "2026-09-15",
|
||||||
|
outdated: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source_id: "labor_mfg",
|
||||||
|
name: "중소제조업 직종별 임금",
|
||||||
|
publisher: "중소기업중앙회",
|
||||||
|
where: "",
|
||||||
|
latest_published: null,
|
||||||
|
our_edition: "2026-07-01",
|
||||||
|
outdated: null,
|
||||||
|
},
|
||||||
|
];
|
||||||
const banners = [
|
const banners = [
|
||||||
outdatedBanner(laborSource),
|
outdatedBanner(laborSources),
|
||||||
outdatedBanner({ ...laborSource, outdated: false }),
|
outdatedBanner([{ ...laborSources[0], outdated: false }]),
|
||||||
outdatedBanner(undefined),
|
outdatedBanner(undefined),
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -225,10 +239,12 @@ def test_칸_글자와_숨김_열(tmp_path: Path) -> None:
|
|||||||
assert [row[1] for row in got["editions"]] == ["2026-01-01", "판 모름", "판 모름"]
|
assert [row[1] for row in got["editions"]] == ["2026-01-01", "판 모름", "판 모름"]
|
||||||
# 단추 글자·칸 설명의 값 — 쉼표 + 단위(이름표) · 단위 없으면 수만
|
# 단추 글자·칸 설명의 값 — 쉼표 + 단위(이름표) · 단위 없으면 수만
|
||||||
assert got["withUnit"] == ["17 원", "41,500,000 원", "0.17", "12,345.678", "", "5억 미만"]
|
assert got["withUnit"] == ["17 원", "41,500,000 원", "0.17", "12,345.678", "", "5억 미만"]
|
||||||
# 날짜가 뒤처져 보여도 서버가 outdated=false 면 띠 없음 · 출처가 안 오면 띠 없음
|
# 뒤처진 원본만 한 줄씩 · 판정을 모르는 것(outdated null)은 안 세움 · 없으면 빈 목록
|
||||||
assert got["banners"] == [
|
assert got["banners"] == [
|
||||||
"이 자료는 2026-01-01 판 · 최신은 2026-09-01 공표 · "
|
[
|
||||||
"대한건설협회 cak.or.kr [지원·사업]>[건설적산기준]>[건설임금]",
|
"건설업 시중노임단가 — 이 자료는 2026-01-01 판 · 최신은 2026-09-01 공표 · "
|
||||||
None,
|
"대한건설협회 cak.or.kr [지원·사업] > [건설적산기준] > [건설임금]"
|
||||||
None,
|
],
|
||||||
|
[],
|
||||||
|
[],
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -148,10 +148,6 @@ export const ui_locales_b3 = {
|
|||||||
Z01_MasterData_Base_oil: ["유가", "Fuel"],
|
Z01_MasterData_Base_oil: ["유가", "Fuel"],
|
||||||
Z01_MasterData_Base_rate: ["요율", "Rates"],
|
Z01_MasterData_Base_rate: ["요율", "Rates"],
|
||||||
Z01_MasterData_Overrides: ["고친 것 모아 보기", "Edited values"],
|
Z01_MasterData_Overrides: ["고친 것 모아 보기", "Edited values"],
|
||||||
Z01_MasterData_Rate_Notice: [
|
|
||||||
"법이 정한 값 — 고치면 갱신 때 주인을 잃기 쉬움",
|
|
||||||
"Values set by law — edits may lose their row when the law is updated",
|
|
||||||
],
|
|
||||||
Z01_MasterData_Edit_Can: [
|
Z01_MasterData_Edit_Can: [
|
||||||
"고칠 수 있는 칸 — 눌러 고치고 Enter 로 저장 (Esc 취소)",
|
"고칠 수 있는 칸 — 눌러 고치고 Enter 로 저장 (Esc 취소)",
|
||||||
"Editable — click, type, Enter to save (Esc cancels)",
|
"Editable — click, type, Enter to save (Esc cancels)",
|
||||||
|
|||||||
Reference in New Issue
Block a user