Files
Aislo/B03_FileInput/B03_FileInput_UI_PlanImport.ts
T
eomsangdonandClaude Opus 5.5 80d363c3aa feat(531-3): D04 확정 화면 · B03 [예정노선에서 가져오기] · 예정노선 바뀜 띠
- D04 화면 — 연도 표(연장 · 시점 · 종점 · 누가 · 확정 · 지정 업체) · 업체 = 등록 회사 찾기 또는 이름 + 메일 · 캐시 d04-assignees · [저장] PUT · [확정] 연도 고름 → finalize(잠금판) · [내보내기] zip
- PUT plan/assignees 에 years — 업체를 다 뺀 연도도 맞춤
- B03 노선 칸 옆 단추 · 머리 띠(새 파일에서 끼움 · 기존 동작 그대로) — 연결 = existing 길 · 옛 결과 지울 때 확인 창

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 20:03:58 +09:00

182 lines
6.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* =============================================================================
* 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 로 다시.
* ========================================================================== */
import { API_BASE_URL, CURRENT_PROJECT_ID_KEY } from "@config/config_frontend";
import {
createButton,
el,
hideLoadingOverlay,
showConfirmDialog,
showLoadingOverlay,
showToast,
} from "@ui/ui_template_elements";
import { t } from "@ui/ui_template_locale";
import { openModal } from "@ui/ui_template_modal";
interface AssignedRoute {
assignee_id: number;
project_name: string;
year: number;
linked_project_id: string | null;
linked_project_name: string | null;
update_available: boolean;
}
interface PlanSource {
linked: boolean;
assignee_id?: number;
plan_name?: string;
year?: number;
update_available?: boolean;
}
async function getJson<T>(path: string): Promise<T> {
const response = await fetch(`${API_BASE_URL}${path}`, { credentials: "include" });
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return (await response.json()) as T;
}
/** 연결 — 지울 결과가 있으면 한 번 묻고 다시 · 성공 = 참 */
async function linkRoute(
assigneeId: number,
projectId: string,
lasFree: boolean,
): Promise<boolean> {
const send = (confirmReplace: boolean): Promise<Response> =>
fetch(`${API_BASE_URL}/dashboard/plan-routes/${assigneeId}/link`, {
method: "POST",
credentials: "include",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
mode: "existing",
project_id: projectId,
confirm_replace: confirmReplace,
las_free: lasFree,
}),
});
showLoadingOverlay();
try {
let response = await send(false);
let data = (await response.json().catch(() => ({}))) as {
confirm?: string;
message?: string;
detail?: unknown;
};
if (response.status === 409 && data.confirm === "replace_outputs") {
hideLoadingOverlay();
const agreed = await showConfirmDialog(data.message ?? "", t("D04_Import_Btn"));
showLoadingOverlay();
if (!agreed) return false;
response = await send(true);
data = (await response.json().catch(() => ({}))) as typeof data;
}
if (!response.ok) {
throw new Error(typeof data.detail === "string" ? data.detail : `HTTP ${response.status}`);
}
showToast(t("D04_Import_Done"), "success");
return true;
} catch (error) {
showToast(error instanceof Error ? error.message : String(error), "error");
return false;
} finally {
hideLoadingOverlay();
}
}
/** 노선 칸에 단추 · 본문 위에 바뀜 띠를 끼움 — `onImported` = 카드 현황 다시 읽기.
* 프로젝트 · 「LAS 없이 설계」는 B03 화면과 같은 기억값(localStorage)을 읽음. */
export function mountPlanImport(
buttonHost: HTMLElement,
bannerHost: HTMLElement,
onImported: () => void,
): void {
const projectId = localStorage.getItem(CURRENT_PROJECT_ID_KEY) ?? "";
if (!projectId) return;
const lasFree = (): boolean => localStorage.getItem(`b03_las_free_${projectId}`) === "1";
const banner = el("div", { className: "b03-file__overview-pending b03-file__plan-update" });
banner.hidden = true;
bannerHost.prepend(banner);
const done = async (assigneeId: number): Promise<void> => {
if (!(await linkRoute(assigneeId, projectId, lasFree()))) return;
onImported();
void refreshBanner();
};
async function refreshBanner(): Promise<void> {
const source = await getJson<PlanSource>(`/projects/${projectId}/plan-source`).catch(
() => null,
);
banner.hidden = !source?.update_available;
if (!source?.update_available || source.assignee_id === undefined) return;
const assigneeId = source.assignee_id;
banner.replaceChildren(
el("span", { text: `${t("D04_Update_Banner")} (${source.plan_name} ${source.year}) — ` }),
createButton({
label: t("D04_Update_Reimport"),
variant: "ghost",
size: "sm",
onClick: () => void done(assigneeId),
}),
);
}
const openPicker = async (): Promise<void> => {
let routes: AssignedRoute[];
try {
routes = (await getJson<{ routes: AssignedRoute[] }>("/dashboard/plan-routes/assigned"))
.routes;
} catch (error) {
showToast(error instanceof Error ? error.message : String(error), "error");
return;
}
const { close } = openModal({
title: t("D04_Import_Title"),
closeLabel: "×",
mount: (body) => {
if (!routes.length) {
body.append(el("p", { text: t("D04_Import_Empty") }));
return;
}
for (const route of routes) {
const linked = route.linked_project_name
? ` · ${t("D04_Linked")}: ${route.linked_project_name}`
: "";
body.append(
el("div", {
className: "b03-file__plan-pick",
children: [
el("span", { text: `${route.project_name} · ${route.year}${linked}` }),
createButton({
label: t("D04_Import_Btn"),
variant: "filled",
size: "sm",
onClick: () => {
close();
void done(route.assignee_id);
},
}),
],
}),
);
}
},
});
};
buttonHost.append(
createButton({
label: t("D04_Import_Btn"),
variant: "ghost",
onClick: () => void openPicker(),
}),
);
void refreshBanner();
}