Merge remote-tracking branch 'origin/sub_laptop_2' into main_laptop_1

This commit is contained in:
2026-10-25 19:59:28 +09:00
8 changed files with 132 additions and 7 deletions
+8
View File
@@ -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,
+2
View File
@@ -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;
@@ -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<AssignedPlan[]> {
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,
);
}
+20 -1
View File
@@ -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<void> {
allJoinRequests: [],
auditLogs: [],
resources: null,
assignedPlans: [],
adminCompanyId: null,
};
await loadRoleData(state);
@@ -134,12 +142,14 @@ async function loadRoleData(state: DashboardState): Promise<void> {
});
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<typeof L>[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(),
+19 -3
View File
@@ -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]),
},
);
}
+1 -2
View File
@@ -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 },
+1 -1
View File
@@ -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"]
}
+11
View File
@@ -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<string, LocaleEntry>;