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

This commit is contained in:
2026-10-25 19:35:54 +09:00
5 changed files with 155 additions and 3 deletions
+2
View File
@@ -15,6 +15,8 @@ export interface SessionUser {
role?: string;
company_id?: number | null;
is_master?: boolean;
/** 산림청 = 시스템관리자 · *.go.kr 메일(요구 345) — 예정노선 만들기 */
is_forest_service?: boolean;
}
async function post(path: string, body?: object): Promise<ApiResult> {
@@ -0,0 +1,48 @@
/* 대시보드 [+] 고르기 모달(528-2) — 카드 넷 한 줄 · 그림 선 = 글자 색(밝음 · 어두움) */
.b01-new-project__grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: var(--spacing-12);
}
.b01-new-project__card {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
gap: var(--spacing-8);
padding: var(--spacing-16) var(--spacing-8);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
background: var(--color-surface);
color: var(--color-text);
font: inherit;
cursor: pointer;
}
.b01-new-project__card:hover:not(.is-dim, .is-soon) {
border-color: var(--color-accent);
color: var(--color-accent);
}
.b01-new-project__card.is-dim,
.b01-new-project__card.is-soon {
opacity: 0.45;
cursor: not-allowed;
}
.b01-new-project__icon svg {
display: block;
width: 56px;
height: 56px;
}
.b01-new-project__badge {
position: absolute;
top: var(--spacing-4);
right: var(--spacing-4);
padding: 0 var(--spacing-4);
border-radius: var(--radius-sm);
background: var(--color-border);
font-size: var(--text-caption);
}
@@ -0,0 +1,89 @@
/* =============================================================================
* B01_Dashboard_UI_NewProject.ts
* 대시보드 [+] 고르기 모달(요구 345 · PLAN 528-2) — 카드 넷(그림 + 제목) = 예정노선 · 임도설계 ·
* 사방설계 · 안정성평가. 그림 = SVG 직접(선 색 = 글자 색 → 밝음 · 어두움 그대로).
* 예정노선 = 산림청(세션 is_forest_service)만 · 아니면 흐림 + 까닭 툴팁(누름 = 같은 까닭 알림)
* 사방설계 = 「준비 중」 · 임도설계 = B02 · 예정노선 = D01 · 안정성평가 = E01
* ========================================================================== */
import "./B01_Dashboard_UI_NewProject.css";
import { ROUTES, type RoutePath } from "@config/config_frontend";
import { el, showToast } from "@ui/ui_template_elements";
import { t, type LocaleKey } from "@ui/ui_template_locale";
import { openModal } from "@ui/ui_template_modal";
import { fetchSessionUser } from "../A06_Login/A06_Login_Api_Fetch";
import { navigateTo } from "../A00_Common/router";
/** 48 칸 그림 — 선 · 면은 currentColor */
const ICONS = {
plan: `<path d="M6 12l12-5 12 5 12-5v29l-12 5-12-5-12 5z"/><path d="M18 7v29M30 12v29"/><path d="M11 33c5-3 8-12 14-12s7 6 12-4" stroke-dasharray="3 3"/><circle cx="11" cy="33" r="2.5" fill="currentColor"/><circle cx="37" cy="17" r="2.5" fill="currentColor"/>`,
road: `<path d="M8 42c6-10 18-6 20-16S20 12 26 6"/><path d="M20 42c6-10 18-6 20-16S32 12 38 6"/><path d="M14 42c6-10 18-6 20-16S26 12 32 6" stroke-dasharray="3 4"/>`,
erosion: `<path d="M4 14c6 6 10 12 10 20M44 14c-6 6-10 12-10 20"/><path d="M12 26h24v8H12z"/><path d="M16 26v8M22 26v8M28 26v8M32 26v8"/><path d="M14 40c4-2 6 2 10 0s6 2 10 0" />`,
stability: `<path d="M4 40h40"/><path d="M4 40V30l12-2 14-16h14v28"/><path d="M10 29a16 16 0 0 0 28-17" stroke-dasharray="3 3"/><path d="M36 14l3-3 1 4" />`,
} as const;
interface CardSpec {
icon: keyof typeof ICONS;
title: LocaleKey;
route?: RoutePath;
/** 못 고르는 까닭(툴팁 · 누름 알림) — 없으면 고를 수 있음 */
reason?: LocaleKey;
soon?: boolean;
}
function svgIcon(kind: keyof typeof ICONS): HTMLElement {
const box = el("span", { className: "b01-new-project__icon", attrs: { "aria-hidden": "true" } });
box.innerHTML = `<svg viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${ICONS[kind]}</svg>`;
return box;
}
function card(spec: CardSpec, close: () => void): HTMLButtonElement {
const button = el("button", {
className: "b01-new-project__card",
attrs: { type: "button" },
children: [svgIcon(spec.icon), el("span", { text: t(spec.title) })],
});
button.dataset.newProject = spec.icon;
if (spec.soon) {
button.classList.add("is-soon");
button.append(el("span", { className: "b01-new-project__badge", text: t("DE_New_Soon") }));
}
if (spec.reason) {
button.classList.add("is-dim");
button.title = t(spec.reason);
}
button.addEventListener("click", () => {
if (spec.reason || spec.soon) return void showToast(t(spec.reason ?? "DE_New_Soon"));
close();
navigateTo(spec.route!);
});
return button;
}
/** [+] — 세션의 산림청 판정으로 예정노선 카드 켬 · 흐림 */
export async function openNewProjectPicker(): Promise<void> {
const forest = Boolean((await fetchSessionUser())?.is_forest_service);
const specs: CardSpec[] = [
{
icon: "plan",
title: "DE_New_Plan",
route: ROUTES.D01_PLAN_REGISTER,
reason: forest ? undefined : "DE_New_PlanForestOnly",
},
{ icon: "road", title: "DE_New_Road", route: ROUTES.B02_PROJ_REGISTER },
{ icon: "erosion", title: "DE_New_Erosion", soon: true },
{ icon: "stability", title: "DE_New_Stability", route: ROUTES.E01_STAB_REGISTER },
];
openModal({
title: t("DE_New_Title"),
closeLabel: t("Common_Btn_Close"),
dialogClass: "b01-new-project",
mount: (body, close) =>
body.append(
el("div", {
className: "b01-new-project__grid",
children: specs.map((spec) => card(spec, close)),
}),
),
});
}
+4 -3
View File
@@ -44,6 +44,7 @@ import {
import { openAddMemberModal, openCreateCompanyModal } from "./B01_Dashboard_UI_Modals";
import { buildProfileForm, buildSecurityForm } from "./B01_Dashboard_UI_Profile";
import { openCompanyPicker } from "./B01_Dashboard_UI_CompanyPick";
import { openNewProjectPicker } from "./B01_Dashboard_UI_NewProject";
import { projectTable } from "./B01_Dashboard_UI_Projects";
import { buildResourcePanel } from "./B01_Dashboard_UI_Resources";
import { buildTempUploadSection } from "./B01_Dashboard_UI_TempUpload";
@@ -260,7 +261,7 @@ function buildPage(state: DashboardState): HTMLElement {
buildSystemSettingsSection(state),
withCompanyPill(
section(L("B01_Dashboard_Projects"), projectTable(state.allProjects, state.user), true, [
createButton({ label: "+", onClick: () => navigateTo(ROUTES.B02_PROJ_REGISTER) }),
createButton({ label: "+", onClick: () => void openNewProjectPicker() }),
]),
state,
),
@@ -294,7 +295,7 @@ function buildPage(state: DashboardState): HTMLElement {
grid.append(
buildSystemSettingsSection(state),
section(L("B01_Dashboard_Projects"), projectTable(state.companyProjects, state.user), true, [
createButton({ label: "+", onClick: () => navigateTo(ROUTES.B02_PROJ_REGISTER) }),
createButton({ label: "+", onClick: () => void openNewProjectPicker() }),
]),
buildTempUploadSection(),
section(L("B01_Dashboard_Members"), memberTable(state.members, state.user), true, [
@@ -307,7 +308,7 @@ function buildPage(state: DashboardState): HTMLElement {
grid.append(
buildSystemSettingsSection(state),
section(L("B01_Dashboard_Projects"), projectTable(state.userProjects, state.user), true, [
createButton({ label: "+", onClick: () => navigateTo(ROUTES.B02_PROJ_REGISTER) }),
createButton({ label: "+", onClick: () => void openNewProjectPicker() }),
]),
// 임시 보관함 — 프로젝트를 만들기 전에 자료를 올려 두는 곳이라 프로젝트 목록
// 바로 아래에 둔다(2026-08-08 사용자 지시).
+12
View File
@@ -18,4 +18,16 @@ export const ui_locales_de = {
DE_Step_Stab3D: ["3D 해석", "3D Analysis"],
DE_Step_StabReport: ["보고서", "Report"],
DE_Step_Empty: ["준비 중인 단계입니다.", "This step is not ready yet."],
/* --- 대시보드 [+] 고르기(528-2) --- */
DE_New_Title: ["새 프로젝트", "New Project"],
DE_New_Plan: ["예정노선", "Planned Route"],
DE_New_Road: ["임도설계", "Forest Road Design"],
DE_New_Erosion: ["사방설계", "Erosion Control"],
DE_New_Stability: ["안정성평가", "Stability Assessment"],
DE_New_Soon: ["준비 중", "Coming soon"],
DE_New_PlanForestOnly: [
"예정노선은 산림청(시스템 관리자 · go.kr 메일)만 만들 수 있습니다.",
"Only the Korea Forest Service (system admin · go.kr mail) can create planned routes.",
],
} as const satisfies Record<string, LocaleEntry>;