From dc124d3108cadac04da20e637d96a642cc2de053 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sun, 25 Oct 2026 19:34:39 +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[+]=20=EA=B3=A0=EB=A5=B4=EA=B8=B0=20=EB=AA=A8?= =?UTF-8?q?=EB=8B=AC=20=E2=80=94=20=EC=B9=B4=EB=93=9C=20=EB=84=B7(?= =?UTF-8?q?=EC=98=88=EC=A0=95=EB=85=B8=EC=84=A0=20=C2=B7=20=EC=9E=84?= =?UTF-8?q?=EB=8F=84=EC=84=A4=EA=B3=84=20=C2=B7=20=EC=82=AC=EB=B0=A9?= =?UTF-8?q?=EC=84=A4=EA=B3=84=20=C2=B7=20=EC=95=88=EC=A0=95=EC=84=B1?= =?UTF-8?q?=ED=8F=89=EA=B0=80)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - [+] 세 자리(시스템관리자 · 회사관리자 · 사용자) = 고르기 모달 - 그림 = SVG 직접(선 = 글자 색 · 밝음/어두움) · 산림청 아니면 예정노선 흐림 + 까닭 툴팁 · 누름 = 같은 알림 - 사방 = 준비 중 · 임도 = B02 · 예정노선 = D01 · 안정성평가 = E01 - 세션 is_forest_service 칸(SessionUser) Co-Authored-By: Claude Opus 5.5 --- A06_Login/A06_Login_Api_Fetch.ts | 2 + B01_Dashboard/B01_Dashboard_UI_NewProject.css | 48 ++++++++++ B01_Dashboard/B01_Dashboard_UI_NewProject.ts | 89 +++++++++++++++++++ B01_Dashboard/B01_Dashboard_UI_Page.ts | 7 +- ui_template/ui_template_locale_de.ts | 12 +++ 5 files changed, 155 insertions(+), 3 deletions(-) create mode 100644 B01_Dashboard/B01_Dashboard_UI_NewProject.css create mode 100644 B01_Dashboard/B01_Dashboard_UI_NewProject.ts diff --git a/A06_Login/A06_Login_Api_Fetch.ts b/A06_Login/A06_Login_Api_Fetch.ts index 4b0088898..116c3b5bc 100644 --- a/A06_Login/A06_Login_Api_Fetch.ts +++ b/A06_Login/A06_Login_Api_Fetch.ts @@ -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 { diff --git a/B01_Dashboard/B01_Dashboard_UI_NewProject.css b/B01_Dashboard/B01_Dashboard_UI_NewProject.css new file mode 100644 index 000000000..51bbf2aab --- /dev/null +++ b/B01_Dashboard/B01_Dashboard_UI_NewProject.css @@ -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); +} diff --git a/B01_Dashboard/B01_Dashboard_UI_NewProject.ts b/B01_Dashboard/B01_Dashboard_UI_NewProject.ts new file mode 100644 index 000000000..0c188793e --- /dev/null +++ b/B01_Dashboard/B01_Dashboard_UI_NewProject.ts @@ -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: ``, + road: ``, + erosion: ``, + stability: ``, +} 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 = `${ICONS[kind]}`; + 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 { + 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)), + }), + ), + }); +} diff --git a/B01_Dashboard/B01_Dashboard_UI_Page.ts b/B01_Dashboard/B01_Dashboard_UI_Page.ts index cdd21d765..ce3d5bdee 100644 --- a/B01_Dashboard/B01_Dashboard_UI_Page.ts +++ b/B01_Dashboard/B01_Dashboard_UI_Page.ts @@ -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 사용자 지시). diff --git a/ui_template/ui_template_locale_de.ts b/ui_template/ui_template_locale_de.ts index 046e4c4a2..d71934712 100644 --- a/ui_template/ui_template_locale_de.ts +++ b/ui_template/ui_template_locale_de.ts @@ -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;