From f558f5d2ba8bca6ab8b48256815ecd2f77a8359b Mon Sep 17 00:00:00 2001 From: umsangdon Date: Tue, 27 Oct 2026 20:39:12 +0900 Subject: [PATCH] =?UTF-8?q?feat(563-2):=20D04=20=ED=91=9C=20=3D=20?= =?UTF-8?q?=EC=A1=B0=EA=B0=81=20=EC=A4=84(=EC=97=B0=EB=8F=84=20=C2=B7=20?= =?UTF-8?q?=EC=84=A0)=20=C2=B7=20=EC=A1=B0=EA=B0=81=EB=A7=88=EB=8B=A4=20?= =?UTF-8?q?=EC=97=85=EC=B2=B4=20=C2=B7=20B03=20=EA=B0=80=EC=A0=B8=EC=98=A4?= =?UTF-8?q?=EA=B8=B0=20=C2=B7=20B01=20=EB=B0=9B=EC=9D=80=20=EC=98=88?= =?UTF-8?q?=EC=A0=95=EB=85=B8=EC=84=A0=20=3D=20=EC=A1=B0=EA=B0=81=20?= =?UTF-8?q?=EC=9D=B4=EB=A6=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - D04 업체 추가 · 저장에 조각 id · 옛 연도 지정 업체 = 그 연도 첫 줄 - B03 [예정노선에서 가져오기] 줄 · B01 받은 예정노선 연도 칸 · [연결] 제목에 「본선 0+0~12+10」 Co-Authored-By: Claude Opus 5.5 --- .../B01_Dashboard_UI_AssignedPlans.ts | 4 +- .../B01_Dashboard_UI_View_Assigned.ts | 9 ++-- B03_FileInput/B03_FileInput_UI_PlanImport.ts | 9 +++- D04_PlanConfirm/D04_PlanConfirm_UI_Page.ts | 52 ++++++++++++++----- 4 files changed, 55 insertions(+), 19 deletions(-) diff --git a/B01_Dashboard/B01_Dashboard_UI_AssignedPlans.ts b/B01_Dashboard/B01_Dashboard_UI_AssignedPlans.ts index 1b0d07868..18849f6ed 100644 --- a/B01_Dashboard/B01_Dashboard_UI_AssignedPlans.ts +++ b/B01_Dashboard/B01_Dashboard_UI_AssignedPlans.ts @@ -1,6 +1,6 @@ /* ============================================================================= * B01_Dashboard_UI_AssignedPlans.ts - * 「받은 예정노선」 자료 읽기(요구서 345 2-2 · PLAN 528-2) — 우리 회사에 지정된 예정노선 연도. + * 「받은 예정노선」 자료 읽기(요구서 345 2-2 · PLAN 528-2 · 563-2) — 우리 회사에 지정된 예정노선 조각(옛 = 연도). * 화면 = `B01_Dashboard_UI_View_Assigned.ts` · [연결] 모달 = `B01_Dashboard_UI_PlanLink` * ========================================================================== */ @@ -11,6 +11,8 @@ export interface AssignedPlan { project_id: string; project_name: string; year: number; + /** 조각 이름 「본선 0+0~12+10」 — 옛 연도 지정 = null */ + piece_label: string | null; linked_project_id: string | null; /** 연도 확정 시각 — 서버 UTC */ confirmed_at: string | null; diff --git a/B01_Dashboard/B01_Dashboard_UI_View_Assigned.ts b/B01_Dashboard/B01_Dashboard_UI_View_Assigned.ts index 37f84ff68..43213c7ac 100644 --- a/B01_Dashboard/B01_Dashboard_UI_View_Assigned.ts +++ b/B01_Dashboard/B01_Dashboard_UI_View_Assigned.ts @@ -24,7 +24,7 @@ import type { ViewCtx } from "./B01_Dashboard_UI_Views"; const COLUMNS: Column[] = [ { label: L("DE_New_Plan") }, - { label: L("DE_Assigned_Year"), width: "90px" }, + { label: L("DE_Assigned_Year"), width: "220px" }, { label: L("D04_Col_Confirmed"), width: "150px" }, { label: L("DE_Assigned_State"), width: "220px" }, { label: "", width: "44px" }, @@ -46,7 +46,7 @@ function planRow(plan: AssignedPlan): Cell[] { const view = (): void => void goToWorkflowStage(plan.project_id, ROUTES.D02_PLAN_BASE); return [ nameCell(plan.project_name), - String(plan.year), + plan.piece_label ? `${plan.year} · ${plan.piece_label}` : String(plan.year), kstText(plan.confirmed_at), stateCell(plan), rowMenu([ @@ -54,7 +54,10 @@ function planRow(plan: AssignedPlan): Cell[] { { label: L("DE_Assigned_Link"), onClick: () => - void openPlanLinkModal(plan.assignee_id, `${plan.project_name} ${plan.year}`), + void openPlanLinkModal( + plan.assignee_id, + [plan.project_name, plan.year, plan.piece_label].filter(Boolean).join(" "), + ), }, ]), ]; diff --git a/B03_FileInput/B03_FileInput_UI_PlanImport.ts b/B03_FileInput/B03_FileInput_UI_PlanImport.ts index 548f660b8..ae6793d7c 100644 --- a/B03_FileInput/B03_FileInput_UI_PlanImport.ts +++ b/B03_FileInput/B03_FileInput_UI_PlanImport.ts @@ -1,7 +1,7 @@ /* ============================================================================= * B03_FileInput_UI_PlanImport.ts * 예정노선에서 가져오기(요구서 345 3-5 · PLAN 531-3) — 본보기 `B03_FileInput_UI_TempPicker.ts`. - * 노선 칸 옆 [예정노선에서 가져오기] = 우리 회사에 지정된 확정 연도 고르기 → + * 노선 칸 옆 [예정노선에서 가져오기] = 우리 회사에 지정된 확정 조각(옛 = 연도) 고르기 → * POST /api/dashboard/plan-routes/{assignee}/link {mode: existing, project_id: 이 프로젝트}. * 머리 띠 = 이어 받은 예정노선이 바뀌면 「예정노선이 바뀜 — [다시 가져오기]」(같은 길 · 자동 반영 없음). * 옛 결과가 지워지면 서버가 409 confirm 으로 묻고 → 확인 창 → confirm_replace 로 다시. @@ -25,6 +25,8 @@ interface AssignedRoute { assignee_id: number; project_name: string; year: number; + /** 조각 이름 「본선 0+0~12+10」 — 옛 연도 지정 = null */ + piece_label: string | null; linked_project_id: string | null; linked_project_name: string | null; update_available: boolean; @@ -157,7 +159,10 @@ export function mountPlanImport( : ""; body.append( createListRow({ - label: `${route.project_name} · ${route.year}${linked}`, + label: [route.project_name, route.year, route.piece_label] + .filter(Boolean) + .join(" · ") + .concat(linked), trailing: createButton({ label: t("D04_Import_Btn"), variant: "filled", diff --git a/D04_PlanConfirm/D04_PlanConfirm_UI_Page.ts b/D04_PlanConfirm/D04_PlanConfirm_UI_Page.ts index 654cc0f3b..c66aa69a2 100644 --- a/D04_PlanConfirm/D04_PlanConfirm_UI_Page.ts +++ b/D04_PlanConfirm/D04_PlanConfirm_UI_Page.ts @@ -1,10 +1,11 @@ /* ============================================================================= * D04_PlanConfirm_UI_Page.ts - * D04 확정 · 업체 · 내보내기(요구서 345 3-4 · PLAN 531-3) — B05~B09 틀(group_workflow). - * 본문 = 연도 표(연장 · 시점 · 종점 · 측점 · 확정 · 지정 업체) · 연도마다 업체 여럿 + * D04 확정 · 업체 · 내보내기(요구서 345 3-4 · 요구 391 · PLAN 531-3 · 563-2) — B05~B09 틀(group_workflow). + * 본문 = 조각 표(연도 · 선 · 연장 · 시점 · 종점 · 측점 · 확정 · 지정 업체) · 조각마다 업체 여럿 + * (조각 없는 옛 프로젝트 = 연도 한 줄 · 옛 연도 지정 업체는 그 연도 첫 줄에 보임) * (등록 회사 찾기 = 가입 화면 회사 검색 · 아니면 업체명 + 메일 수기). * 좌측 = 확정할 연도 고르기 + [내보내기] [확정] [저장]. - * 조작 = 캐시 `d04-assignees` 즉시 · [저장] = PUT …/plan/assignees(연도 전부 맞춤) → 캐시 비움 · + * 조작 = 캐시 `d04-assignees` 즉시 · [저장] = PUT …/plan/assignees(연도 전부 맞춤 · 조각 id) → 캐시 비움 · * [확정] = (캐시 있으면 저장 먼저) POST …/plan/finalize → 잠금판 · [내보내기] = GET …/plan/export zip. * 산림청이 아니면(지정 회사 = 보기만) 업체 고침 · 연도 고르기 · [확정] · [저장] 없음 · [내보내기]만. * ========================================================================== */ @@ -51,8 +52,15 @@ interface YearRow { end?: EndPoint; } +/** 표 한 줄 — 조각(piece_id) · 옛 연도(piece_id null) */ +interface PieceRow extends YearRow { + piece_id: string | null; + line_name: string | null; +} + interface Assignee { year: number; + piece_id?: string | null; company_id: number | null; company_name: string | null; email: string | null; @@ -63,6 +71,7 @@ interface Assignee { interface Summary { name: string; years: YearRow[]; + pieces: PieceRow[]; assignees: Assignee[]; } @@ -87,9 +96,10 @@ const STATION_INTERVAL_M = 20; const station = (m: number | undefined): string => m === undefined || m === null ? "—" : stationLabel(m, STATION_INTERVAL_M, 1); const where = (p?: EndPoint): string => (p ? `${p.lat.toFixed(6)}, ${p.lon.toFixed(6)}` : "—"); -/** 같은 업체 — 등록 회사 = id · 수기 = 메일(서버 `_key` 와 같음) */ +/** 같은 업체 — 같은 조각(옛 = 연도)에서 등록 회사 = id · 수기 = 메일(서버 `_key` 와 같음) */ const sameFirm = (a: Assignee, b: Assignee): boolean => a.year === b.year && + (a.piece_id ?? null) === (b.piece_id ?? null) && (a.company_id !== null ? a.company_id === b.company_id : b.company_id === null && (a.email ?? "").toLowerCase() === (b.email ?? "").toLowerCase()); @@ -105,7 +115,7 @@ export async function renderD04PlanConfirm(root: HTMLElement): Promise { const panel = el("div", { className: "d04-confirm__panel", children: [pickCard, actions] }); mountGroupStep(root, ROUTES.D04_PLAN_CONFIRM, { mainContent: main, leftPanel: panel }); - let summary: Summary = { name: "", years: [], assignees: [] }; + let summary: Summary = { name: "", years: [], pieces: [], assignees: [] }; let editable = false; // 산림청만 고침 const draft = (): Assignee[] | null => editable ? readState("d04-assignees", projectId) : null; @@ -123,8 +133,9 @@ export async function renderD04PlanConfirm(root: HTMLElement): Promise { async function save(): Promise { const body = { years: summary.years.map((row) => row.year), - assignees: current().map(({ year, company_id, company_name, email }) => ({ + assignees: current().map(({ year, piece_id, company_id, company_name, email }) => ({ year, + piece_id: piece_id ?? null, company_id, company_name, email, @@ -146,8 +157,8 @@ export async function renderD04PlanConfirm(root: HTMLElement): Promise { } } - function addForm(year: number): HTMLElement { - const listId = `d04-companies-${year}`; + function addForm(row: PieceRow): HTMLElement { + const listId = `d04-companies-${row.piece_id ?? row.year}`; const options = el("datalist", { attrs: { id: listId } }); const found = new Map(); const firm = createInputField({ placeholder: t("D04_Company"), size: 28 }).input; @@ -182,7 +193,13 @@ export async function renderD04PlanConfirm(root: HTMLElement): Promise { showToast(t("D04_NeedEmail"), "warning"); return; } - const next: Assignee = { year, company_id: companyId, company_name: name, email }; + const next: Assignee = { + year: row.year, + piece_id: row.piece_id, + company_id: companyId, + company_name: name, + email, + }; const list = current(); if (list.some((item) => sameFirm(item, next))) return; edit([...list, next]); @@ -227,21 +244,30 @@ export async function renderD04PlanConfirm(root: HTMLElement): Promise { })); const { root: tableRoot, tbody } = tableShell(columns); tableRoot.querySelector("table")?.classList.add("d04-confirm__table"); - for (const row of summary.years) { - const firms = list.filter((item) => item.year === row.year).map(firmChip); + const firstOfYear = new Set( + summary.pieces.filter((row, i, all) => all.findIndex((o) => o.year === row.year) === i), + ); + // 조각 지정 = 그 조각 줄 · 옛 연도 지정(piece_id 없음) = 그 연도 첫 줄 + const belongs = (item: Assignee, row: PieceRow): boolean => + item.piece_id + ? item.piece_id === row.piece_id + : item.year === row.year && firstOfYear.has(row); + for (const row of summary.pieces) { + const firms = list.filter((item) => belongs(item, row)).map(firmChip); const chain = row.saved ? `${station(row.start_chainage_m)} ~ ${station(row.end_chainage_m)}` : t("D04_NotSaved"); const tr = tableRow(columns, [ - String(row.year), + row.line_name ? `${row.year} · ${row.line_name}` : String(row.year), fmt(row.length_m), where(row.start), where(row.end), chain, kstText(row.confirmed_at), - el("div", { children: editable ? [...firms, addForm(row.year)] : firms }), + el("div", { children: editable ? [...firms, addForm(row)] : firms }), ]); tr.dataset.year = String(row.year); + if (row.piece_id) tr.dataset.piece = row.piece_id; tbody.append(tr); } const notes = draft() ? [createBanner({ tone: "warning", text: t("D04_Dirty") })] : [];