Files
Aislo/B03_FileInput/B03_FileInput_UI_PlanImport.ts
T
eomsangdonandClaude Sonnet 5.5 a826804277 refactor(B02·B03): 페이지 자체 변종을 부품 상자 부품으로 — 파일 카드 · 띠 알림 · 진행 막대 · 켜기 · 패널 머리 · 단추 (PLAN 553-1)
- B03 파일 카드 = 카드 부품 · [선택] · [×] = 단추 · 아이콘 단추 · 진행 막대 · 오류 띠 = 부품
- B03 현황 · 이어올리기 · 예정노선 바뀜 = 띠 알림 · LAS 없이 설계 = 켜기 · 노선 · 지형 제목 = 패널 머리
- B03 · B02 죽은 CSS 걷음 · 날 색 → 공통 값 · 가이드 촬영 선택자 · 지킴 시험 test_553_1_pages

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-26 21:53:32 +09:00

186 lines
6.2 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 { createBanner } from "@ui/ui_template_banner";
import {
createButton,
el,
hideLoadingOverlay,
showConfirmDialog,
showLoadingOverlay,
showToast,
} from "@ui/ui_template_elements";
import { t } from "@ui/ui_template_locale";
import { createListRow } from "@ui/ui_template_list_row";
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__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(
createBanner({
tone: "info",
text: `${t("D04_Update_Banner")} (${source.plan_name} ${source.year})`,
actions: [
createButton({
label: t("D04_Update_Reimport"),
variant: "ghost",
size: 24,
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(
createListRow({
label: `${route.project_name} · ${route.year}${linked}`,
trailing: createButton({
label: t("D04_Import_Btn"),
variant: "filled",
size: 24,
onClick: () => {
close();
void done(route.assignee_id);
},
}),
}),
);
}
},
});
};
buttonHost.append(
createButton({
label: t("D04_Import_Btn"),
variant: "ghost",
onClick: () => void openPicker(),
}),
);
void refreshBanner();
}