From 244a2bf8eb424e997f10c58c39f71a19f4df3c10 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sun, 25 Oct 2026 19:57:13 +0900 Subject: [PATCH] =?UTF-8?q?feat(528-2):=20=EB=8C=80=EC=8B=9C=EB=B3=B4?= =?UTF-8?q?=EB=93=9C=20=ED=91=9C=20=EC=A2=85=EB=A5=98=20=EC=97=B4=20=C2=B7?= =?UTF-8?q?=20=EC=A2=85=EB=A5=98=EB=B3=84=20=EB=8B=A8=EA=B3=84=20=EB=A7=89?= =?UTF-8?q?=EB=8C=80=20=C2=B7=20=E3=80=8C=EB=B0=9B=EC=9D=80=20=EC=98=88?= =?UTF-8?q?=EC=A0=95=EB=85=B8=EC=84=A0=E3=80=8D=20=EA=B5=AC=EC=97=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 프로젝트 표 「종류」 열(임도설계 · 예정노선 · 안정성평가 · 사방설계 · 칸 없음 = 임도) - 단계 막대 = 종류별(ROAD B03~B09 그대로 · PLAN D02~D04 · STABILITY E02~E05) · 누름 = 그 단계 화면 - 받은 예정노선(회사관리자 · 사용자 · 받은 것 있을 때만) = 예정노선 · 연도 · 연결 상태 · 업데이트 배지 · [보기] = D02 · [연결] 자리만(531-3) - E01 [등록] 고르기 칸 setError 없음 오류 걷음(목록 비면 단추 꺼짐) - tsconfig D0* · E0* → 폴더 안까지(D0*/**/*) — 앞 판은 폴더 안 파일을 안 잡아 타입 검사가 빠졌음 Co-Authored-By: Claude Opus 5.5 --- A00_Common/group_workflow.ts | 8 +++ B01_Dashboard/B01_Dashboard_Api_Fetch.ts | 2 + .../B01_Dashboard_UI_AssignedPlans.ts | 70 +++++++++++++++++++ B01_Dashboard/B01_Dashboard_UI_Page.ts | 21 +++++- B01_Dashboard/B01_Dashboard_UI_Projects.ts | 22 +++++- E01_StabRegister/E01_StabRegister_UI_Page.ts | 3 +- tsconfig.json | 2 +- ui_template/ui_template_locale_de.ts | 11 +++ 8 files changed, 132 insertions(+), 7 deletions(-) create mode 100644 B01_Dashboard/B01_Dashboard_UI_AssignedPlans.ts diff --git a/A00_Common/group_workflow.ts b/A00_Common/group_workflow.ts index a056c69cf..0fa1d7e21 100644 --- a/A00_Common/group_workflow.ts +++ b/A00_Common/group_workflow.ts @@ -36,6 +36,14 @@ const FLOWS: readonly GroupFlow[] = [ }, ]; +/** 종류별 단계 묶음(대시보드 표 단계 막대 · 528-2) — ROAD · 모르는 종류 = 없음(B 막대 그대로) */ +export function groupFlowOf( + kind: string | null | undefined, +): { routes: readonly RoutePath[]; steps: string[]; icons: readonly string[] } | null { + const flow = { PLAN: FLOWS[0], STABILITY: FLOWS[1] }[kind ?? ""]; + return flow ? { ...flow, steps: flow.labels.map((key) => t(key)) } : null; +} + /** 단계 틀을 세움 — mainContent · leftPanel 이 없으면 빈 단계 안내 · [?] 줄만 */ export function mountGroupStep( root: HTMLElement, diff --git a/B01_Dashboard/B01_Dashboard_Api_Fetch.ts b/B01_Dashboard/B01_Dashboard_Api_Fetch.ts index a4d41a743..bc3fbb138 100644 --- a/B01_Dashboard/B01_Dashboard_Api_Fetch.ts +++ b/B01_Dashboard/B01_Dashboard_Api_Fetch.ts @@ -39,6 +39,8 @@ export interface ProjectItem { id: string; company_id?: number | null; name: string; + /** 종류(요구 345) — ROAD 임도 · PLAN 예정노선 · STABILITY 안정성평가 · EROSION 사방 */ + project_kind?: string | null; region?: string | null; road_type?: string | null; project_year?: number | null; diff --git a/B01_Dashboard/B01_Dashboard_UI_AssignedPlans.ts b/B01_Dashboard/B01_Dashboard_UI_AssignedPlans.ts new file mode 100644 index 000000000..f5ab0a7ad --- /dev/null +++ b/B01_Dashboard/B01_Dashboard_UI_AssignedPlans.ts @@ -0,0 +1,70 @@ +/* ============================================================================= + * B01_Dashboard_UI_AssignedPlans.ts + * 대시보드 「받은 예정노선」 구역(요구서 345 2-2 · PLAN 528-2) — 우리 회사에 지정된 예정노선 연도. + * 열 = 예정노선 · 연도 · 연결 상태(연결됨 · 미연결 · 업데이트 있음 배지) · [보기] [연결] + * [보기] = 그 예정노선 첫 단계(D02 · 공유 회사 = GET 만) · [연결] = 자리만(동작은 531-3) + * ========================================================================== */ + +import { API_BASE_URL, ROUTES } from "@config/config_frontend"; +import { createButton, createTag } from "@ui/ui_template_elements"; +import { table, text } from "@ui/ui_template_general_blocks"; +import { goToWorkflowStage } from "../A00_Common/b_workflow_nav"; +import { DASHBOARD_VISIBLE_ROWS, L } from "./B01_Dashboard_UI_Common"; + +export interface AssignedPlan { + project_id: string; + project_name: string; + year: number; + linked_project_id: string | null; + update_available: boolean; +} + +/** 받은 예정노선 — 실패 · 없음 = 빈 목록(구역을 안 그림) */ +export async function fetchAssignedPlans(): Promise { + const response = await fetch(`${API_BASE_URL}/dashboard/plan-routes/assigned`, { + credentials: "include", + }); + if (!response.ok) return []; + return ((await response.json()) as { routes?: AssignedPlan[] }).routes ?? []; +} + +function linkState(plan: AssignedPlan): HTMLElement { + const cell = document.createElement("div"); + cell.className = "b01-dashboard__actions"; + cell.append( + createTag( + L(plan.linked_project_id ? "DE_Assigned_Linked" : "DE_Assigned_Unlinked"), + plan.linked_project_id ? "success" : "neutral", + ), + ); + if (plan.update_available) cell.append(createTag(L("DE_Assigned_Update"), "warning")); + return cell; +} + +export function assignedPlanTable(plans: AssignedPlan[]): HTMLElement { + return table( + [ + L("DE_New_Plan"), + L("DE_Assigned_Year"), + L("DE_Assigned_State"), + L("B01_Dashboard_Table_Action"), + ], + plans.map((plan) => { + const actions = document.createElement("div"); + actions.className = "b01-dashboard__actions"; + const link = createButton({ label: L("DE_Assigned_Link"), variant: "ghost" }); + link.disabled = true; + link.title = L("DE_New_Soon"); + actions.append( + createButton({ + label: L("DE_Assigned_View"), + variant: "ghost", + onClick: () => void goToWorkflowStage(plan.project_id, ROUTES.D02_PLAN_BASE), + }), + link, + ); + return [text(plan.project_name), text(plan.year), linkState(plan), actions]; + }), + DASHBOARD_VISIBLE_ROWS, + ); +} diff --git a/B01_Dashboard/B01_Dashboard_UI_Page.ts b/B01_Dashboard/B01_Dashboard_UI_Page.ts index ce3d5bdee..770e60116 100644 --- a/B01_Dashboard/B01_Dashboard_UI_Page.ts +++ b/B01_Dashboard/B01_Dashboard_UI_Page.ts @@ -45,6 +45,11 @@ import { openAddMemberModal, openCreateCompanyModal } from "./B01_Dashboard_UI_M import { buildProfileForm, buildSecurityForm } from "./B01_Dashboard_UI_Profile"; import { openCompanyPicker } from "./B01_Dashboard_UI_CompanyPick"; import { openNewProjectPicker } from "./B01_Dashboard_UI_NewProject"; +import { + assignedPlanTable, + fetchAssignedPlans, + type AssignedPlan, +} from "./B01_Dashboard_UI_AssignedPlans"; import { projectTable } from "./B01_Dashboard_UI_Projects"; import { buildResourcePanel } from "./B01_Dashboard_UI_Resources"; import { buildTempUploadSection } from "./B01_Dashboard_UI_TempUpload"; @@ -65,6 +70,8 @@ export interface DashboardState { allJoinRequests: JoinRequest[]; auditLogs: AuditLog[]; resources: ResourceData | null; + /** 우리 회사에 지정된 예정노선(528-2) — 회사관리자 · 사용자만 */ + assignedPlans: AssignedPlan[]; /** 시스템 관리자가 고른 회사(29-1) — 프로젝트 · 사용자 · 가입 요청 · 회사 관리가 이 회사 것 */ adminCompanyId: number | null; } @@ -96,6 +103,7 @@ export async function renderB01Dashboard(root: HTMLElement): Promise { allJoinRequests: [], auditLogs: [], resources: null, + assignedPlans: [], adminCompanyId: null, }; await loadRoleData(state); @@ -134,12 +142,14 @@ async function loadRoleData(state: DashboardState): Promise { }); return; } - const [projects, company] = await Promise.all([ + const [projects, company, assigned] = await Promise.all([ fetchUserProjects().catch(() => []), fetchUserCompany().catch(() => null), + fetchAssignedPlans().catch(() => []), ]); state.userProjects = projects; state.company = company; + state.assignedPlans = assigned; if (state.user.role === "ADMIN") { const [members, requests, companyProjects] = await Promise.all([ fetchCompanyMembers().catch(() => []), @@ -219,6 +229,7 @@ const SECTION_ICONS: [Parameters[0], string][] = [ ["B01_Dashboard_Resources", "📊"], ["B01_Dashboard_SystemSettings", "⚙️"], ["B01_Dashboard_Projects", "🗂️"], + ["DE_Assigned_Title", "📬"], ["B01_Temp_Section", "📦"], ["B01_Dashboard_Users", "👥"], ["B01_Dashboard_Members", "👥"], @@ -246,6 +257,12 @@ function mountSectionShortcuts(grid: HTMLElement): void { setDrawerSteps(bar); } +/** 받은 예정노선 구역 — 받은 것이 있을 때만 */ +function assignedPlanSection(state: DashboardState): HTMLElement[] { + if (!state.assignedPlans.length) return []; + return [section(L("DE_Assigned_Title"), assignedPlanTable(state.assignedPlans), true)]; +} + function buildPage(state: DashboardState): HTMLElement { // 제목·여백은 공용 템플릿을 따른다 (2026-09-06 사용자 지시) — B02 등 다른 화면과 같은 모양. const page = document.createElement("div"); @@ -297,6 +314,7 @@ function buildPage(state: DashboardState): HTMLElement { section(L("B01_Dashboard_Projects"), projectTable(state.companyProjects, state.user), true, [ createButton({ label: "+", onClick: () => void openNewProjectPicker() }), ]), + ...assignedPlanSection(state), buildTempUploadSection(), section(L("B01_Dashboard_Members"), memberTable(state.members, state.user), true, [ createButton({ label: "+", onClick: () => openAddMemberModal() }), @@ -310,6 +328,7 @@ function buildPage(state: DashboardState): HTMLElement { section(L("B01_Dashboard_Projects"), projectTable(state.userProjects, state.user), true, [ createButton({ label: "+", onClick: () => void openNewProjectPicker() }), ]), + ...assignedPlanSection(state), // 임시 보관함 — 프로젝트를 만들기 전에 자료를 올려 두는 곳이라 프로젝트 목록 // 바로 아래에 둔다(2026-08-08 사용자 지시). buildTempUploadSection(), diff --git a/B01_Dashboard/B01_Dashboard_UI_Projects.ts b/B01_Dashboard/B01_Dashboard_UI_Projects.ts index a07fe8071..313aeac3f 100644 --- a/B01_Dashboard/B01_Dashboard_UI_Projects.ts +++ b/B01_Dashboard/B01_Dashboard_UI_Projects.ts @@ -3,15 +3,27 @@ import { table, text } from "@ui/ui_template_general_blocks"; import { createStepBar } from "@ui/ui_template_workflow_layout"; import { workflowSteps } from "../A00_Common/b_page_scaffold"; import { goToWorkflowStage, WORKFLOW_STEP_ROUTES } from "../A00_Common/b_workflow_nav"; +import { groupFlowOf } from "../A00_Common/group_workflow"; import type { DashboardUser, ProjectItem } from "./B01_Dashboard_Api_Fetch"; import { canDeleteProject, canEditProject } from "./B01_Dashboard_UI_Helper"; import { openDeleteProjectModal, openEditProjectModal } from "./B01_Dashboard_UI_Modals"; import { DASHBOARD_VISIBLE_ROWS, formatDate, L } from "./B01_Dashboard_UI_Common"; +/** 종류 열 글(528-2) — 칸이 없는 옛 응답 = 임도 */ +const KIND_LABEL = { + ROAD: "DE_New_Road", + PLAN: "DE_New_Plan", + STABILITY: "DE_New_Stability", + EROSION: "DE_New_Erosion", +} as const; +const kindText = (project: ProjectItem): string => + L(KIND_LABEL[(project.project_kind ?? "ROAD") as keyof typeof KIND_LABEL] ?? "DE_New_Road"); + export function projectTable(projects: ProjectItem[], currentUser: DashboardUser): HTMLElement { return table( [ L("B01_Dashboard_Table_Project"), + L("DE_Kind_Column"), L("B01_Dashboard_Table_Region"), // 진행도(%) 열은 없앴다 (2026-09-06 사용자 지시) — 워크플로 배지가 같은 것을 보여 준다. L("B01_Dashboard_Table_Workflow"), @@ -43,6 +55,7 @@ export function projectTable(projects: ProjectItem[], currentUser: DashboardUser return [ text(project.name), + text(kindText(project)), text(project.region), workflow(project), text(formatDate(project.updated_at)), @@ -53,16 +66,19 @@ export function projectTable(projects: ProjectItem[], currentUser: DashboardUser ); } +/** 단계 막대 — 종류별 단계(ROAD = B03~B09 · PLAN = D02~D04 · STABILITY = E02~E05) · 누름 = 그 단계 화면 */ export function workflow(project: ProjectItem): HTMLElement { + const group = groupFlowOf(project.project_kind); + const routes = group?.routes ?? WORKFLOW_STEP_ROUTES; return createStepBar( - workflowSteps(), + group?.steps ?? workflowSteps(), project.workflow_state?.current_stage ?? project.workflow_stage, { stages: project.workflow_state?.stages, currentStage: project.workflow_state?.current_stage, - routes: WORKFLOW_STEP_ROUTES, + routes, compact: true, - onStepClick: (stepIndex) => goToWorkflowStage(project.id, WORKFLOW_STEP_ROUTES[stepIndex]), + onStepClick: (stepIndex) => goToWorkflowStage(project.id, routes[stepIndex]), }, ); } diff --git a/E01_StabRegister/E01_StabRegister_UI_Page.ts b/E01_StabRegister/E01_StabRegister_UI_Page.ts index 6425818f2..461f78469 100644 --- a/E01_StabRegister/E01_StabRegister_UI_Page.ts +++ b/E01_StabRegister/E01_StabRegister_UI_Page.ts @@ -46,8 +46,7 @@ export function renderE01StabRegister(root: HTMLElement): void { label: t("DE_Reg_Submit"), variant: "filled", onClick: () => { - source.setError(); - if (!source.select.value) return void source.setError(t("B02_Proj_Error_Required")); + // 목록이 비면 단추가 꺼져 있음 — 고른 값은 늘 있음 void createGroupProject( "/e01/project", { project_name: name.input.value.trim() || null, source_project_id: source.select.value }, diff --git a/tsconfig.json b/tsconfig.json index c2c101de7..2964abb60 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -34,6 +34,6 @@ "@kninnug/constrainautor": ["config/node_modules/@kninnug/constrainautor"], } }, - "include": ["A00_Common", "A0*", "B0*", "B1*", "M0*", "D0*", "E0*", "Z0*", "ui_template", "config/**/*", "common_util"], + "include": ["A00_Common", "A0*", "B0*", "B1*", "M0*", "D0*/**/*", "E0*/**/*", "Z0*", "ui_template", "config/**/*", "common_util"], "exclude": ["0_old", "node_modules", "venv", "dist"] } diff --git a/ui_template/ui_template_locale_de.ts b/ui_template/ui_template_locale_de.ts index 93d459f31..3f4f1d1f7 100644 --- a/ui_template/ui_template_locale_de.ts +++ b/ui_template/ui_template_locale_de.ts @@ -62,4 +62,15 @@ export const ui_locales_de = { ], DE_Reg_Submit: ["등록", "Register"], DE_Reg_Success: ["프로젝트를 만들었습니다.", "Project created."], + + /* --- 대시보드 표 · 받은 예정노선(528-2) --- */ + DE_Kind_Column: ["종류", "Kind"], + DE_Assigned_Title: ["받은 예정노선", "Assigned Planned Routes"], + DE_Assigned_Year: ["연도", "Year"], + DE_Assigned_State: ["연결 상태", "Link"], + DE_Assigned_Linked: ["연결됨", "Linked"], + DE_Assigned_Unlinked: ["미연결", "Not linked"], + DE_Assigned_Update: ["업데이트 있음", "Update available"], + DE_Assigned_View: ["보기", "View"], + DE_Assigned_Link: ["연결", "Link"], } as const satisfies Record;