From a8268042771ec5c642d77751dcc1dca46a2ac849 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 26 Oct 2026 21:53:32 +0900 Subject: [PATCH] =?UTF-8?q?refactor(B02=C2=B7B03):=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20=EC=9E=90=EC=B2=B4=20=EB=B3=80=EC=A2=85=EC=9D=84=20?= =?UTF-8?q?=EB=B6=80=ED=92=88=20=EC=83=81=EC=9E=90=20=EB=B6=80=ED=92=88?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=E2=80=94=20=ED=8C=8C=EC=9D=BC=20=EC=B9=B4?= =?UTF-8?q?=EB=93=9C=20=C2=B7=20=EB=9D=A0=20=EC=95=8C=EB=A6=BC=20=C2=B7=20?= =?UTF-8?q?=EC=A7=84=ED=96=89=20=EB=A7=89=EB=8C=80=20=C2=B7=20=EC=BC=9C?= =?UTF-8?q?=EA=B8=B0=20=C2=B7=20=ED=8C=A8=EB=84=90=20=EB=A8=B8=EB=A6=AC=20?= =?UTF-8?q?=C2=B7=20=EB=8B=A8=EC=B6=94=20(PLAN=20553-1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - B03 파일 카드 = 카드 부품 · [선택] · [×] = 단추 · 아이콘 단추 · 진행 막대 · 오류 띠 = 부품 - B03 현황 · 이어올리기 · 예정노선 바뀜 = 띠 알림 · LAS 없이 설계 = 켜기 · 노선 · 지형 제목 = 패널 머리 - B03 · B02 죽은 CSS 걷음 · 날 색 → 공통 값 · 가이드 촬영 선택자 · 지킴 시험 test_553_1_pages Co-Authored-By: Claude Sonnet 5.5 --- A05_EduDetail/guide/B03/_scenes.json | 6 +- .../B02_ProjRegister_UI_Style.css | 17 -- B03_FileInput/B03_FileInput_State.ts | 8 +- B03_FileInput/B03_FileInput_UI_Page.ts | 125 +++++++------ B03_FileInput/B03_FileInput_UI_Page_Flow.ts | 13 +- B03_FileInput/B03_FileInput_UI_PlanImport.ts | 46 ++--- B03_FileInput/B03_FileInput_UI_Style.css | 166 +++--------------- B03_FileInput/B03_FileInput_UI_Style_Card.css | 158 +++++------------ B03_FileInput/B03_FileInput_UI_Style_Temp.css | 2 +- B03_FileInput/B03_FileInput_UI_Support.ts | 12 +- resources/tester/test_553_1_pages.py | 59 +++++++ 11 files changed, 244 insertions(+), 368 deletions(-) create mode 100644 resources/tester/test_553_1_pages.py diff --git a/A05_EduDetail/guide/B03/_scenes.json b/A05_EduDetail/guide/B03/_scenes.json index a3d912b18..3d6328975 100644 --- a/A05_EduDetail/guide/B03/_scenes.json +++ b/A05_EduDetail/guide/B03/_scenes.json @@ -38,11 +38,11 @@ }, { "n": 6, - "sel": ".b03-file__columns > :nth-child(1) .b03-file__group-title" + "sel": ".b03-file__columns > :nth-child(1) .ui-panel-head__title" }, { "n": 7, - "sel": ".b03-file__columns > :nth-child(2) .b03-file__group-title" + "sel": ".b03-file__columns > :nth-child(2) .ui-panel-head__title" } ] }, @@ -215,7 +215,7 @@ "wait": ".b03-file__lasfree" }, { - "click": ".b03-file__lasfree input" + "click": ".b03-file__lasfree" }, { "wait_ms": 800 diff --git a/B02_ProjRegister/B02_ProjRegister_UI_Style.css b/B02_ProjRegister/B02_ProjRegister_UI_Style.css index 4186b3f80..b3f6ebcfc 100644 --- a/B02_ProjRegister/B02_ProjRegister_UI_Style.css +++ b/B02_ProjRegister/B02_ProjRegister_UI_Style.css @@ -19,23 +19,6 @@ gap: var(--spacing-16); } -/* select는 공통 .ui-input 상속 + 드롭다운 화살표 여백 확보 */ -.b02-proj__select { - appearance: none; - cursor: pointer; - background-image: - linear-gradient(45deg, transparent 50%, var(--color-slate) 50%), - linear-gradient(135deg, var(--color-slate) 50%, transparent 50%); - background-position: - calc(100% - 18px) 50%, - calc(100% - 13px) 50%; - background-size: - 5px 5px, - 5px 5px; - background-repeat: no-repeat; - padding-right: var(--spacing-32); -} - @media (max-width: 640px) { .b02-proj__grid { grid-template-columns: 1fr; diff --git a/B03_FileInput/B03_FileInput_State.ts b/B03_FileInput/B03_FileInput_State.ts index 5e26d331b..eae8fa787 100644 --- a/B03_FileInput/B03_FileInput_State.ts +++ b/B03_FileInput/B03_FileInput_State.ts @@ -1,3 +1,5 @@ +import { createBanner } from "@ui/ui_template_banner"; + export type B03AnalysisStatus = "pending" | "in_progress" | "completed" | "failed"; export interface B03StoredFileState { @@ -78,9 +80,9 @@ export function restoreB03ProjectState(options: { const state = loadB03ProjectState(options.projectId); if (!state) return; options.container.replaceChildren(); - const message = document.createElement("span"); - message.textContent = `${options.label}: ${state.analysisProgress}%`; - options.container.append(message); + options.container.append( + createBanner({ tone: "info", text: `${options.label}: ${state.analysisProgress}%` }), + ); options.container.classList.add("is-visible"); if (state.analysisStatus === "in_progress" || state.analysisStatus === "pending") { void options.poll(options.projectId).then((completed) => { diff --git a/B03_FileInput/B03_FileInput_UI_Page.ts b/B03_FileInput/B03_FileInput_UI_Page.ts index f9de843c3..4a63ac235 100644 --- a/B03_FileInput/B03_FileInput_UI_Page.ts +++ b/B03_FileInput/B03_FileInput_UI_Page.ts @@ -5,7 +5,12 @@ import { UPLOAD_MAX_FILES, } from "@config/config_frontend"; import { currentLanguageIndex, ui_locales } from "@ui/ui_template_locale"; -import { createButton, createTag } from "@ui/ui_template_elements"; +import { createBanner } from "@ui/ui_template_banner"; +import { createButton, createIconButton, createTag } from "@ui/ui_template_elements"; +import { createPanelHead } from "@ui/ui_template_panel_head"; +import { createProgressBar, type ProgressBarHandle } from "@ui/ui_template_progress"; +import { createText } from "@ui/ui_template_text"; +import { createToggle } from "@ui/ui_template_toggle"; import { createGeneralLayout } from "@ui/ui_template_general_layout"; import { createWorkflowOverlays } from "@ui/ui_template_overlay"; import { createStepBar, WORKFLOW_STEP_ICONS } from "@ui/ui_template_workflow_layout"; @@ -63,6 +68,7 @@ export async function renderB03FileInput(root: HTMLElement): Promise { // 대시보드 임시 보관함에서 가져올 자료 선택기 — 선택되면 [업로드]가 이동을 수행한다. const tempPicker = createTempPicker(() => updateUploadButton()); const cardMap = new Map(); + const progressMap = new Map(); let uploadButton: HTMLButtonElement; let resumeBanner: HTMLDivElement; let activeProjectId = localStorage.getItem(CURRENT_PROJECT_ID_KEY) ?? ""; @@ -75,9 +81,8 @@ export async function renderB03FileInput(root: HTMLElement): Promise { ? localStorage.getItem(`b03_las_free_${activeProjectId}`) === "1" : false; - const subtitle = document.createElement("p"); - subtitle.className = "b03-file__subtitle"; - subtitle.textContent = L("B03_File_Subtitle"); + const subtitle = createText(L("B03_File_Subtitle"), "muted", 14); + subtitle.classList.add("b03-file__subtitle"); const fileInput = document.createElement("input"); fileInput.type = "file"; @@ -94,7 +99,7 @@ export async function renderB03FileInput(root: HTMLElement): Promise { dropzone.append(dropzoneLabel, fileInput); const pageError = document.createElement("p"); - pageError.className = "b03-file__error"; + pageError.className = "ui-text ui-text--error b03-file__error"; pageError.setAttribute("role", "alert"); // 업로드 버튼 안에서 도는 원 — 공용 스피너를 버튼 크기로 줄여 쓴다. @@ -200,11 +205,11 @@ export async function renderB03FileInput(root: HTMLElement): Promise { const fileName = card.querySelector(".b03-file__file-name"); const fileSize = card.querySelector(".b03-file__file-size"); - const progress = card.querySelector(".b03-file__progress-bar"); + const progress = progressMap.get(slot); const progressBytes = card.querySelector(".b03-file__progress-bytes"); const progressSpeed = card.querySelector(".b03-file__progress-speed"); const progressEta = card.querySelector(".b03-file__progress-eta"); - const error = card.querySelector(".b03-file__error-message"); + const error = card.querySelector(".b03-file__error-message .ui-banner__text"); const remove = card.querySelector(".b03-file__card-remove"); const percent = state.file ? Math.min(100, (state.progressBytes / state.file.size) * 100) : 0; @@ -217,7 +222,7 @@ export async function renderB03FileInput(root: HTMLElement): Promise { ? `${state.serverUploaded.sizeMb.toFixed(2)} MB` : ""; } - if (progress) progress.style.width = `${percent}%`; + progress?.set(percent / 100); if (progressBytes) { progressBytes.textContent = `${L("B03_File_Progress_Bytes")}: ${formatBytes( state.progressBytes, @@ -413,26 +418,27 @@ export async function renderB03FileInput(root: HTMLElement): Promise { const lock = overview.analysis_lock; if (lock?.running) { const who = lock.name ?? lock.email ?? null; - const busy = document.createElement("p"); - busy.className = "b03-file__overview-lock"; - busy.textContent = who - ? `${who} 님이 올린 자료를 분석하는 중입니다 — 끝난 뒤에 새 자료를 올릴 수 있습니다.` - : "다른 자리에서 올린 자료를 분석하는 중입니다 — 끝난 뒤에 새 자료를 올릴 수 있습니다."; - notes.push(busy); + notes.push( + createBanner({ + tone: "warning", + text: who + ? `${who} 님이 올린 자료를 분석하는 중입니다 — 끝난 뒤에 새 자료를 올릴 수 있습니다.` + : "다른 자리에서 올린 자료를 분석하는 중입니다 — 끝난 뒤에 새 자료를 올릴 수 있습니다.", + }), + ); } if (overview.required_complete && overview.analysis_complete) { - const complete = document.createElement("p"); - complete.className = "b03-file__overview-complete"; - complete.textContent = L("B03_File_Overview_Complete"); - notes.push(complete); + notes.push(createBanner({ tone: "success", text: L("B03_File_Overview_Complete") })); } for (const session of overview.pending_sessions) { - const pending = document.createElement("p"); - pending.className = "b03-file__overview-pending"; - pending.textContent = - `${session.original_filename} — ${session.progress_percent}% ` + - L("B03_File_Overview_Pending"); - notes.push(pending); + notes.push( + createBanner({ + tone: "info", + text: + `${session.original_filename} — ${session.progress_percent}% ` + + L("B03_File_Overview_Pending"), + }), + ); } if (notes.length) { overviewBanner.append(...notes); @@ -453,18 +459,30 @@ export async function renderB03FileInput(root: HTMLElement): Promise { renderExtensionLabel(card, state); const input = card.querySelector(".b03-file__slot-input")!; input.accept = state.extensions.join(","); - const select = card.querySelector(".b03-file__card-select")!; - select.textContent = L("B03_File_Card_Select"); + const select = createButton({ label: L("B03_File_Card_Select"), variant: "ghost", size: 24 }); + select.classList.add("b03-file__card-select"); + card.querySelector('[data-slot="select"]')!.replaceWith(select); select.addEventListener("click", () => input.click()); input.addEventListener("change", () => { onFileSelected(input.files ? Array.from(input.files) : [], state.slot); input.value = ""; }); - const remove = card.querySelector(".b03-file__card-remove")!; - remove.textContent = "×"; - remove.title = L("B03_File_Card_Remove"); - remove.setAttribute("aria-label", L("B03_File_Card_Remove")); - remove.addEventListener("click", () => removeFile(state.slot)); + const remove = createIconButton({ + glyph: "×", + title: L("B03_File_Card_Remove"), + variant: "outline", + size: 24, + onClick: () => removeFile(state.slot), + }); + remove.classList.add("b03-file__card-remove"); + card.querySelector('[data-slot="remove"]')!.replaceWith(remove); + const progress = createProgressBar({ thick: 6 }); + progress.root.classList.add("b03-file__progress"); + card.querySelector('[data-slot="progress"]')!.replaceWith(progress.root); + progressMap.set(state.slot, progress); + const errorBanner = createBanner({ tone: "danger", text: "" }); + errorBanner.classList.add("b03-file__error-message"); + card.querySelector('[data-slot="error"]')!.replaceWith(errorBanner); cardMap.set(state.slot, card); return card; } @@ -478,18 +496,7 @@ export async function renderB03FileInput(root: HTMLElement): Promise { const group = document.createElement("section"); group.className = "b03-file__group"; if (modifier) group.classList.add(modifier); - if (title) { - const groupTitle = document.createElement("h3"); - groupTitle.className = "b03-file__group-title"; - groupTitle.textContent = title; - group.append(groupTitle); - } - if (hint) { - const groupHint = document.createElement("p"); - groupHint.className = "b03-file__group-hint"; - groupHint.textContent = hint; - group.append(groupHint); - } + if (title) group.append(createPanelHead({ title, sub: hint }).root); const content = document.createElement("div"); content.className = "b03-file__group-content"; for (const slot of groupSlots) { @@ -560,7 +567,8 @@ export async function renderB03FileInput(root: HTMLElement): Promise { pickRow.append(dropzone, tempPicker.root, uploadButton); const uploadControlPanel = document.createElement("div"); - uploadControlPanel.className = "b03-file__control-panel"; + uploadControlPanel.dataset.pad = "16"; + uploadControlPanel.className = "ui-card b03-file__control-panel"; uploadControlPanel.append(subtitle, pickRow, resumeBanner, pageError); // 자료의 출처가 둘로 갈린다 — 원청이 준 계획노선, 측량이 준 지형(LAS·래스터). @@ -581,16 +589,17 @@ export async function renderB03FileInput(root: HTMLElement): Promise { ); // LAS 없는 설계 토글 — 켜면 포인트클라우드 카드를 비활성화하고 필수에서 뺀다. - const lasFreeRow = document.createElement("label"); - lasFreeRow.className = "b03-file__lasfree"; + const lasFreeToggle = createToggle({ + label: L("B03_File_LasFree_Toggle"), + checked: lasFreeDesign, + size: 36, + onChange: onLasFreeChange, + }); + const lasFreeRow = lasFreeToggle.root; + lasFreeRow.classList.add("b03-file__lasfree"); lasFreeRow.title = L("B03_File_LasFree_Hint"); - const lasFreeCheck = document.createElement("input"); - lasFreeCheck.type = "checkbox"; - const lasFreeText = document.createElement("span"); - lasFreeText.textContent = L("B03_File_LasFree_Toggle"); - lasFreeRow.append(lasFreeCheck, lasFreeText); function applyLasFreeState(): void { - lasFreeCheck.checked = lasFreeDesign; + lasFreeToggle.set(lasFreeDesign); terrainGroup.classList.toggle("b03-file__group--disabled", lasFreeDesign); for (const slot of TERRAIN_SLOTS) { const card = cardMap.get(slot); @@ -608,8 +617,8 @@ export async function renderB03FileInput(root: HTMLElement): Promise { if (lasFreeDesign && slots.get(slot)?.file) removeFile(slot); } } - lasFreeCheck.addEventListener("change", () => { - lasFreeDesign = lasFreeCheck.checked; + function onLasFreeChange(checked: boolean): void { + lasFreeDesign = checked; if (activeProjectId) { localStorage.setItem(`b03_las_free_${activeProjectId}`, lasFreeDesign ? "1" : "0"); } @@ -619,19 +628,21 @@ export async function renderB03FileInput(root: HTMLElement): Promise { // 골라 놓고도 [파일 업로드]가 잠긴 채 남는다(2026-09-03 실측: 파일을 먼저 고르고 // 토글을 나중에 켠 순서에서 재현). updateUploadButton(); - }); + } // LAS 토글은 **파일 입력 컨테이너**의 것이다(2026-09-03 사용자 지시) — 무엇을 받을지 // 정하는 스위치라 고르는 자리 옆에 선다. 켜면 지형 컨테이너가 통째로 잠긴다. uploadControlPanel.insertBefore(lasFreeRow, resumeBanner); const routePanel = document.createElement("div"); - routePanel.className = "b03-file__control-panel b03-file__cards-container-panel"; + routePanel.dataset.pad = "16"; + routePanel.className = "ui-card b03-file__control-panel b03-file__cards-container-panel"; routePanel.append(routeGroup); mountPlanImport(routePanel, uploadControlPanel, () => void applyUploadOverview()); const terrainPanel = document.createElement("div"); - terrainPanel.className = "b03-file__control-panel b03-file__cards-container-panel"; + terrainPanel.dataset.pad = "16"; + terrainPanel.className = "ui-card b03-file__control-panel b03-file__cards-container-panel"; terrainPanel.append(terrainGroup); const cardsContainer = document.createElement("div"); diff --git a/B03_FileInput/B03_FileInput_UI_Page_Flow.ts b/B03_FileInput/B03_FileInput_UI_Page_Flow.ts index bc8c31045..d00b1902a 100644 --- a/B03_FileInput/B03_FileInput_UI_Page_Flow.ts +++ b/B03_FileInput/B03_FileInput_UI_Page_Flow.ts @@ -8,6 +8,7 @@ * ========================================================================== */ import { CURRENT_PROJECT_ID_KEY, type RoutePath } from "@config/config_frontend"; +import { createBanner } from "@ui/ui_template_banner"; import { createButton, showToast } from "@ui/ui_template_elements"; import { currentLanguageIndex, ui_locales } from "@ui/ui_template_locale"; import { navigateTo } from "../A00_Common/router"; @@ -81,16 +82,16 @@ export function createUploadFlow(ctx: UploadFlowContext): UploadFlowHandle { state.uploadSessionId = session.uploadSessionId; state.progressBytes = session.completedChunks * session.chunkSizeBytes; ctx.renderSlot(state.slot); - const text = document.createElement("span"); - text.textContent = `${L("B03_File_Status_Detected")}: ${session.fileName}`; const resume = createButton({ label: L("B03_File_Resume_Button"), variant: "ghost", + size: 24, onClick: () => void startChunkedUpload([state]), }); const fresh = createButton({ label: L("B03_File_New_Button"), variant: "ghost", + size: 24, onClick: () => { localStorage.removeItem(session.key); state.uploadSessionId = undefined; @@ -100,7 +101,13 @@ export function createUploadFlow(ctx: UploadFlowContext): UploadFlowHandle { ctx.resumeBanner.classList.remove("is-visible"); }, }); - ctx.resumeBanner.append(text, resume, fresh); + ctx.resumeBanner.append( + createBanner({ + tone: "info", + text: `${L("B03_File_Status_Detected")}: ${session.fileName}`, + actions: [resume, fresh], + }), + ); ctx.resumeBanner.classList.add("is-visible"); break; } diff --git a/B03_FileInput/B03_FileInput_UI_PlanImport.ts b/B03_FileInput/B03_FileInput_UI_PlanImport.ts index 9c26aac8b..548f660b8 100644 --- a/B03_FileInput/B03_FileInput_UI_PlanImport.ts +++ b/B03_FileInput/B03_FileInput_UI_PlanImport.ts @@ -8,6 +8,7 @@ * ========================================================================== */ import { API_BASE_URL, CURRENT_PROJECT_ID_KEY } from "@config/config_frontend"; +import { createBanner } from "@ui/ui_template_banner"; import { createButton, el, @@ -17,6 +18,7 @@ import { 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 { @@ -99,7 +101,7 @@ export function mountPlanImport( 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" }); + const banner = el("div", { className: "b03-file__plan-update" }); banner.hidden = true; bannerHost.prepend(banner); @@ -117,12 +119,17 @@ export function mountPlanImport( 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), + 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), + }), + ], }), ); } @@ -149,20 +156,17 @@ export function mountPlanImport( ? ` · ${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); - }, - }), - ], + 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); + }, + }), }), ); } diff --git a/B03_FileInput/B03_FileInput_UI_Style.css b/B03_FileInput/B03_FileInput_UI_Style.css index fb01e0657..ae9a2ca63 100644 --- a/B03_FileInput/B03_FileInput_UI_Style.css +++ b/B03_FileInput/B03_FileInput_UI_Style.css @@ -1,10 +1,6 @@ .b03-file { min-height: calc(100dvh - var(--app-header-height, 0px)); - background: linear-gradient( - 180deg, - var(--color-canvas, #ffffff) 0%, - var(--color-mist-violet, #edecff) 100% - ); + background: linear-gradient(180deg, var(--color-canvas) 0%, var(--color-accent-soft) 100%); padding: 0 var(--spacing-24) var(--spacing-48) var(--spacing-24); /* 상단 여백은 main-layout 마진으로 조정하므로 padding-top은 0 */ } @@ -27,31 +23,17 @@ display: flex; flex-direction: column; gap: var(--spacing-16); - background: var(--color-canvas, #ffffff); - /* 여백 32 → 16 (2026-09-03) — 고르는 자리만 남기고 안내를 좌측 패널로 뺐다. - `--spacing-20`은 테마에 없는 토큰이라 선언째 무효가 된다(패딩이 통째로 사라짐). */ - padding: var(--spacing-16); - border-radius: var(--radius-large, 24px); /* Wiza 24px 대형 둥글기 */ - border: 1px solid var(--color-mist, #e6e2e3); - box-shadow: var(--shadow-lg); } .b03-file__cards-container-panel { margin-top: 0; /* 단일 layout gap에 의해 제어되도록 설정 */ } -.b03-file__subtitle { - font-size: var(--text-body-sm, 14px); - color: var(--color-slate, #615e6e); - margin: 0 0 var(--spacing-8) 0; -} - .b03-file__native-input, .b03-file__slot-input { display: none; } -/* Wiza 스타일 가이드의 Lavender Glow 그라데이션 및 soft purple border hover */ /* 고르는 자리 한 줄 — [입력 파일 선택] · [임시 보관함] · [파일 업로드](2026-09-03). 좁아지면 아래로 접히되, 선택 영역이 남는 폭을 먹는다. */ /* 셋을 같은 폭·같은 높이로 나눈다 — 열 1:1:1(2026-09-03 사용자 지시). @@ -80,9 +62,9 @@ 패널로 갔으므로 여기는 「끌어 놓거나 눌러서 고르는 자리」 표시만 한다. */ .b03-file__dropzone { padding: var(--spacing-12) var(--spacing-16); - border: 1px dashed var(--color-mist, #e6e2e3); - border-radius: var(--radius-cards, 8px); - background: var(--color-paper, #f6f7fa); + border: 1px dashed var(--color-border); + border-radius: var(--radius-cards); + background: var(--color-surface); display: flex; flex-direction: row; flex-wrap: wrap; @@ -92,100 +74,54 @@ cursor: pointer; text-align: center; transition: - background var(--transition-base, 0.2s), - border-color var(--transition-base, 0.2s), - box-shadow var(--transition-base, 0.2s); + background var(--transition-base), + border-color var(--transition-base), + box-shadow var(--transition-base); } .b03-file__dropzone:hover { - background: var(--color-mist-violet, #edecff); - border-color: var(--color-royal-amethyst, #3e0079); + background: var(--color-accent-soft); + border-color: var(--color-accent); } .b03-file__dropzone:focus-visible, .b03-file__dropzone.is-dragging { outline: none; - background: var(--color-mist-violet, #edecff); - border-color: var(--color-royal-amethyst, #3e0079); - box-shadow: 0 0 0 4px rgba(62, 0, 121, 0.15); + background: var(--color-accent-soft); + border-color: var(--color-accent); + box-shadow: 0 0 0 4px color-mix(in srgb, var(--color-accent) 15%, transparent); } .b03-file__dropzone strong { - color: var(--color-deep-iris, #26114a); - font-size: var(--text-body-sm, 14px); - font-weight: var(--font-weight-medium, 500); + color: var(--color-text); + font-size: var(--text-body-sm); + font-weight: var(--font-weight-medium); } .b03-file__dropzone span { - font-size: var(--text-body-sm, 14px); - color: var(--color-slate, #615e6e); -} - -/* 빈 오류 줄이 자리를 잡고 있으면 토글 아래가 통째로 비어 보인다 — 글자가 있을 때만 - 자리를 차지한다(2026-09-03 사용자 지시). */ -.b03-file__error { - color: var(--color-danger, #dc2626); - font-size: var(--text-body-sm, 14px); - margin: 0; + font-size: var(--text-body-sm); + color: var(--color-text-secondary); } +/* 빈 오류 줄이 자리를 잡고 있으면 토글 아래가 통째로 비어 보인다 — 글자가 있을 때만 자리를 차지한다. */ .b03-file__error:empty { display: none; } -.b03-file__resume { - display: none; - align-items: center; - gap: var(--spacing-8); - flex-wrap: wrap; - padding: var(--spacing-12) var(--spacing-16); - border: 1px solid var(--color-mist, #e6e2e3); - border-radius: var(--radius-cards, 8px); - background: var(--color-paper, #f6f7fa); - color: var(--color-deep-iris, #26114a); - font-size: var(--text-body-sm, 14px); - margin-top: var(--spacing-8); -} - -.b03-file__resume.is-visible { - display: flex; -} - -/* 재접속 현황(서버 정본) 배너 — 완료 안내와 중단 세션 이어올리기 안내를 담는다. */ +/* 띠 알림(부품)을 담는 자리 — 글이 있을 때(is-visible)만 선다. */ +.b03-file__resume, .b03-file__overview { display: none; flex-direction: column; gap: var(--spacing-4); margin-top: var(--spacing-8); - padding: var(--spacing-12) var(--spacing-16); - border: 1px solid var(--color-border); - border-radius: var(--radius-cards, 8px); - background: var(--color-surface); - font-size: var(--text-body-sm, 14px); } +.b03-file__resume.is-visible, .b03-file__overview.is-visible { display: flex; } -.b03-file__overview-complete { - margin: 0; - color: var(--color-success, #3fbb6f); - font-weight: 600; -} - -.b03-file__overview-pending { - margin: 0; - color: var(--color-text-secondary); -} - -/* 잠금 표시 — 지금 누가 올리는 중인지. 막힌 까닭이라 눈에 띄게 둔다. */ -.b03-file__overview-lock { - margin: 0; - color: var(--color-warning, #d98324); - font-weight: 600; -} - /* 완료 슬롯 재업로드 확인 모달 — 페이지 위를 덮는 단순 확인창. */ .b03-file__modal-backdrop { position: fixed; @@ -194,7 +130,7 @@ display: flex; align-items: center; justify-content: center; - background: rgb(0 0 0 / 45%); + background: var(--color-scrim); } .b03-file__modal { @@ -204,9 +140,9 @@ max-width: 420px; padding: var(--spacing-24); border: 1px solid var(--color-border); - border-radius: var(--radius-cards, 8px); + border-radius: var(--radius-cards); background: var(--color-surface-raised); - box-shadow: 0 12px 32px rgb(0 0 0 / 30%); + box-shadow: var(--shadow-lg); } .b03-file__modal p { @@ -221,12 +157,6 @@ justify-content: flex-end; } -.b03-file__cards-grid { - display: grid; - grid-template-columns: 1fr; - gap: var(--spacing-32); -} - /* 계획노선 | 지형(LAS) — 컨테이너를 둘로 나눈다(2026-08-31 사용자 지시). 노선은 카드 5장(shapefile 한 벌), 지형은 4장이라 폭을 5:4 비슷하게 준다. @@ -241,39 +171,9 @@ align-items: start; } -/* LAS 없이 설계 토글 — 파일 입력 컨테이너의 스위치(2026-09-03 사용자 지시). - B04 좌측 패널의 체크 줄(`b04-surface__check`)과 같은 서식이다. */ +/* LAS 없이 설계 — 켜기 부품(스위치) · 자리만 정함 */ .b03-file__lasfree { - display: flex; - align-items: center; - gap: var(--spacing-8); align-self: flex-start; - padding: var(--spacing-8) var(--spacing-12); - border: 1px solid var(--color-mist, #e6e2e3); - border-radius: var(--radius-buttons, 8px); - font-size: var(--text-body-sm, 14px); - color: var(--color-deep-iris, #26114a); - cursor: pointer; - transition: border-color var(--transition-base, 0.2s); -} - -.b03-file__lasfree:hover { - border-color: var(--color-royal-amethyst, #3e0079); -} - -.b03-file__lasfree input { - width: 16px; - height: 16px; - margin: 0; - accent-color: var(--color-royal-amethyst, #3e0079); - cursor: pointer; -} - -/* 켜져 있으면 스위치 자신도 켜진 티를 낸다 — 지형 컨테이너가 왜 잠겼는지의 근거. */ -.b03-file__lasfree:has(input:checked) { - border-color: var(--color-royal-amethyst, #3e0079); - background: var(--color-mist-violet, #edecff); - font-weight: var(--font-weight-medium, 500); } /* LAS 없이 설계를 켜면 지형 컨테이너는 받지 않는다 — 잠긴 것이 보이게 흐린다. */ @@ -282,7 +182,7 @@ } .b03-file__card--disabled { - background: var(--color-paper, #f6f7fa); + background: var(--color-surface); border-style: dashed; } @@ -324,20 +224,6 @@ gap: var(--spacing-12); } -.b03-file__group-hint { - font-size: var(--text-body-sm, 14px); - color: var(--color-slate, #615e6e); - margin: calc(var(--spacing-8) * -1) 0 0 0; -} - -/* 제목 24px → 본문 크기(2026-09-03) — 컨테이너가 둘뿐이라 큰 제목이 자리만 먹었다. */ -.b03-file__group-title { - font-size: var(--text-body, 16px); - color: var(--color-deep-iris, #26114a); - font-family: var(--font-britti-sans, inherit); - margin: 0; -} - /* 카드가 좁아지면 한 줄에 제목·상태·[선택]이 못 들어가 제목이 글자 단위로 접힌다 (2026-09-03 실측: 안내 패널을 연 상태에서 카드 폭 210px). 폭이 모자라면 열을 줄인다. */ /* ⚠ `minmax(260px, …)` 는 **칸이 260px 보다 좁아질 수 없다**는 뜻이라, 컨테이너가 diff --git a/B03_FileInput/B03_FileInput_UI_Style_Card.css b/B03_FileInput/B03_FileInput_UI_Style_Card.css index 74cc766e2..4c65d3139 100644 --- a/B03_FileInput/B03_FileInput_UI_Style_Card.css +++ b/B03_FileInput/B03_FileInput_UI_Style_Card.css @@ -14,50 +14,34 @@ width: 14px; height: 14px; border-width: 2px; - border-color: rgb(255 255 255 / 40%); + border-color: color-mix(in srgb, var(--color-primary-text) 40%, transparent); border-top-color: var(--color-primary-text); } -/* Wiza 8px radius 카드 */ -/* 카드 한 장 = 파일 한 칸. 빈 칸은 **한 줄**로 끝난다(2026-09-03 사용자 지시) — - 파일이 붙거나 올라가는 중에만 아래 내용(파일명·진행바)이 펼쳐진다. */ +/* 카드 한 장 = 파일 한 칸(카드 부품) · 빈 칸은 **한 줄**로 끝난다(2026-09-03 사용자 지시) — + 파일이 붙거나 올라가는 중에만 아래 내용(파일명·진행바)이 펼쳐진다. 상태는 선 색으로 가른다. */ .b03-file__card { - padding: var(--spacing-12) var(--spacing-16); - border: 1px solid var(--color-mist, #e6e2e3); - border-radius: var(--radius-cards, 8px); - background: var(--color-canvas, #ffffff); - box-shadow: var(--shadow-sm); display: flex; flex-direction: column; gap: var(--spacing-8); - transition: - transform var(--transition-base, 0.2s), - border-color var(--transition-base, 0.2s), - box-shadow var(--transition-base, 0.2s); -} - -.b03-file__card:hover { - transform: translateY(-2px); - box-shadow: var(--shadow-lg); - border-color: var(--color-royal-amethyst, #3e0079); } .b03-file__card--selected { - border-color: var(--color-royal-amethyst, #3e0079); - background: var(--color-paper, #f6f7fa); + border-color: var(--color-accent); + background: var(--color-surface); } .b03-file__card--uploading { - border-color: var(--color-royal-amethyst, #3e0079); - box-shadow: var(--shadow-lg-2); + border-color: var(--color-accent); + box-shadow: var(--shadow-lg); } .b03-file__card--completed { - border-color: #10b981; /* Success Green */ + border-color: var(--color-success); } .b03-file__card--error { - border-color: var(--color-danger, #dc2626); + border-color: var(--color-danger); } /* 카드가 좁아졌다(컨테이너 2열 × 카드 2열, 2026-08-31). 격자로 고정하면 제목이 @@ -83,13 +67,13 @@ flex: 0 0 auto; width: 28px; height: 28px; - border-radius: var(--radius-icons, 8px); + border-radius: var(--radius-icons); display: inline-flex; align-items: center; justify-content: center; - color: var(--color-royal-amethyst, #3e0079); - background: var(--color-mist-violet, #edecff); - font-size: var(--text-body-sm, 14px); + color: var(--color-accent); + background: var(--color-accent-soft); + font-size: var(--text-body-sm); margin-right: var(--spacing-8); /* 아이콘 우측 마진 추가 (아이콘 좌측 여유 확대 효과) */ } @@ -102,9 +86,9 @@ } .b03-file__card-label { - color: var(--color-deep-iris, #26114a); - font-size: var(--text-body-sm, 14px); - font-weight: var(--font-weight-medium, 500); + color: var(--color-text); + font-size: var(--text-body-sm); + font-weight: var(--font-weight-medium); /* 제목은 한 줄로 두고 모자라면 말줄임 — 접히면 카드가 세로로 길어진다(2026-09-03). */ white-space: nowrap; overflow: hidden; @@ -114,8 +98,8 @@ .b03-file__card-ext, .b03-file__file-size, .b03-file__progress-info { - color: var(--color-slate, #615e6e); - font-size: var(--text-caption, 12px); + color: var(--color-text-secondary); + font-size: var(--text-caption); } .b03-file__card-badge-container { @@ -125,28 +109,9 @@ margin-right: var(--spacing-4); } -.b03-file__card-remove { - width: 24px; - height: 24px; - border: 1px solid var(--color-mist, #e6e2e3); - border-radius: var(--radius-buttons, 8px); - color: var(--color-slate, #615e6e); - background: var(--color-canvas, #ffffff); - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - font-size: var(--text-body-sm, 14px); - line-height: 1; - padding: 0; - margin-left: var(--spacing-8); /* 취소 버튼 좌측 여유 추가 (취소 버튼 우측 여유 확보) */ - transition: all var(--transition-base, 0.2s); -} - -.b03-file__card-remove:hover { - color: var(--color-danger, #dc2626); - border-color: var(--color-danger, #dc2626); - background: #fef2f2; +/* 파일 없는 칸에서는 지움 단추를 접는다(아이콘 단추 부품은 hidden 보다 display 가 앞섬) */ +.b03-file__card-remove[hidden] { + display: none; } .b03-file__card-content { @@ -160,24 +125,6 @@ display: none; } -/* [선택]은 제목 줄 오른쪽 끝의 작은 버튼 — 종전 전폭 40px 버튼이 카드 높이를 키웠다. */ -.b03-file__card-select { - flex: 0 0 auto; - min-height: 26px; - padding: 0 var(--spacing-8); - border: 1px solid var(--color-mist, #e6e2e3); - border-radius: var(--radius-buttons, 8px); - color: var(--color-deep-iris, #26114a); - background: var(--color-canvas, #ffffff); - font-weight: var(--font-weight-medium, 500); - font-size: var(--text-caption, 12px); - white-space: nowrap; - cursor: pointer; - transition: - background var(--transition-base, 0.2s), - border-color var(--transition-base, 0.2s); -} - .b03-file__file-info { display: flex; flex-direction: column; @@ -186,9 +133,9 @@ } .b03-file__file-name { - color: var(--color-deep-iris, #26114a); - font-size: var(--text-body-sm, 14px); - font-weight: var(--font-weight-medium, 500); + color: var(--color-text); + font-size: var(--text-body-sm); + font-weight: var(--font-weight-medium); overflow-wrap: anywhere; } @@ -204,36 +151,15 @@ display: flex; } -.b03-file__progress-bar-container { - width: 100%; - height: 6px; - overflow: hidden; - border-radius: var(--radius-pills, 1440px); - background: var(--color-mist, #e6e2e3); -} - -.b03-file__progress-bar { - width: 0; - height: 100%; - border-radius: var(--radius-pills, 1440px); - background: var(--color-royal-amethyst, #3e0079); - transition: width var(--transition-base, 0.2s); -} - .b03-file__progress-info { display: flex; flex-direction: column; gap: 2px; } -.b03-file__error-message { +/* 오류 띠(띠 알림 부품)는 오류 난 카드에서만 보인다 */ +.b03-file__card .b03-file__error-message { display: none; - padding: var(--spacing-8) var(--spacing-12); - border-radius: var(--radius-cards, 8px); - color: var(--color-danger, #dc2626); - background: #fef2f2; - font-size: var(--text-caption, 12px); - border: 1px solid rgba(220, 38, 38, 0.15); } .b03-file__card--empty .b03-file__file-info, @@ -260,9 +186,9 @@ width: 100%; height: 120px; overflow: hidden; - border: 1px solid var(--color-border, #dcdce6); - border-radius: var(--radius-cards, 8px); - background: var(--color-surface, #fff); + border: 1px solid var(--color-border); + border-radius: var(--radius-cards); + background: var(--color-surface); } .b03-file__preview svg { @@ -274,7 +200,7 @@ /* 라이다 탑뷰 점 그림 — 점이 5천 개라 선 하나로 묶어 그린다. */ .b03-file__preview-cloud { fill: none; - stroke: var(--color-text-muted, #5a5a72); + stroke: var(--color-text-muted); stroke-linecap: round; stroke-opacity: 0.55; stroke-width: 0.7; @@ -286,7 +212,7 @@ .b03-file__preview-thumb { max-width: 100%; max-height: 100%; - border: 1px dashed var(--color-border, #9aa); + border: 1px dashed var(--color-border); image-rendering: pixelated; object-fit: contain; } @@ -302,24 +228,24 @@ } .b03-file__preview-fact { - color: var(--color-text, #1c1c28); + color: var(--color-text); font-size: var(--text-body, 15px); font-weight: 600; } .b03-file__preview-fact-sub { - color: var(--color-text-muted, #5a5a72); - font-size: var(--text-caption, 12px); + color: var(--color-text-muted); + font-size: var(--text-caption); } .b03-file__preview-empty { - color: var(--color-text-muted, #8a8aa0); - font-size: var(--text-caption, 12px); + color: var(--color-text-muted); + font-size: var(--text-caption); } .b03-file__preview-extent { - fill: color-mix(in srgb, var(--color-border, #9aa) 12%, transparent); - stroke: var(--color-border, #9aa); + fill: color-mix(in srgb, var(--color-border) 12%, transparent); + stroke: var(--color-border); stroke-dasharray: 4 3; stroke-width: 1; } @@ -328,7 +254,7 @@ fill: none; /* 계획노선 선 색 — 녹색(2026-09-04 사용자 지시). 지형 범위를 벗어나면 아래에서 붉은색으로 덮어써 경고로 쓴다. */ - stroke: var(--color-success, #16a34a); + stroke: var(--color-success); stroke-linecap: round; stroke-linejoin: round; stroke-width: 1.5; @@ -336,11 +262,11 @@ /* 계획노선이 지형 자료 범위를 벗어난 경우 — 눈으로 바로 잡히게 경고색. */ .b03-file__preview.is-warning .b03-file__preview-route { - stroke: var(--color-danger, #d33); + stroke: var(--color-danger); } .b03-file__preview.is-warning .b03-file__preview-extent { - stroke: var(--color-danger, #d33); + stroke: var(--color-danger); stroke-dasharray: 3 2; } @@ -355,7 +281,7 @@ } .b03-file__card--error .b03-file__error-message { - display: block; + display: flex; } .b03-file__card--error .b03-file__progress-section { diff --git a/B03_FileInput/B03_FileInput_UI_Style_Temp.css b/B03_FileInput/B03_FileInput_UI_Style_Temp.css index 11edd0e06..c9aa2a262 100644 --- a/B03_FileInput/B03_FileInput_UI_Style_Temp.css +++ b/B03_FileInput/B03_FileInput_UI_Style_Temp.css @@ -68,7 +68,7 @@ .b03-file__temp-option.is-selected { border-color: var(--color-primary); - background: var(--color-mist-violet); + background: var(--color-accent-soft); } .b03-file__temp-option-files { diff --git a/B03_FileInput/B03_FileInput_UI_Support.ts b/B03_FileInput/B03_FileInput_UI_Support.ts index 9caaa008f..79f7b3891 100644 --- a/B03_FileInput/B03_FileInput_UI_Support.ts +++ b/B03_FileInput/B03_FileInput_UI_Support.ts @@ -227,7 +227,7 @@ export function createFileCardTemplate(): HTMLTemplateElement { const template = document.createElement("template"); template.id = "file-card-template"; template.innerHTML = ` -
+
@@ -235,8 +235,8 @@ export function createFileCardTemplate(): HTMLTemplateElement {
- - + +
@@ -246,16 +246,14 @@ export function createFileCardTemplate(): HTMLTemplateElement {
-
-
-
+
- +
`; diff --git a/resources/tester/test_553_1_pages.py b/resources/tester/test_553_1_pages.py new file mode 100644 index 000000000..8d28ada8d --- /dev/null +++ b/resources/tester/test_553_1_pages.py @@ -0,0 +1,59 @@ +"""553-1 B02 · B03 · B04 · B07 페이지 부품 옮김 지킴 시험 — 자체 변종이 부품으로 바뀐 자리 · 가이드 촬영 선택자 · 날 색 없음.""" + +import json +import re +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] +OLD_ALIAS = re.compile(r"--color-(royal-amethyst|deep-iris|mist-violet|plum-velvet|lavender-wash)") +RAW_COLOR = re.compile(r"#[0-9a-fA-F]{3,8}\b|\brgba?\(") + + +def _src(folder: str, suffix: str) -> dict[str, str]: + return {p.name: p.read_text(encoding="utf-8") for p in (ROOT / folder).glob(f"*{suffix}")} + + +def _guide_selectors(page: str, prefix: str) -> set[str]: + text = (ROOT / f"A05_EduDetail/guide/{page}/_scenes.json").read_text(encoding="utf-8") + return set(re.findall(rf"\.{re.escape(prefix)}[a-z0-9_-]+", text)) + + +def _defined(folder: str, name: str) -> bool: + body = "".join(_src(folder, ".ts").values()) + "".join(_src(folder, ".css").values()) + return name.lstrip(".") in body + + +def test_b03_guide_selectors_alive(): + """가이드 촬영 장면이 잡는 B03 클래스는 소스에 살아 있어야 함.""" + for sel in _guide_selectors("B03", "b03-file__"): + assert _defined("B03_FileInput", sel), f"가이드 선택자 {sel} 가 B03 소스에 없음" + + +def test_b03_uses_parts(): + ts = "".join(_src("B03_FileInput", ".ts").values()) + for part in ( + "createBanner", + "createProgressBar", + "createToggle", + "createPanelHead", + "createIconButton", + "createText", + ): + assert part in ts, f"B03 이 부품 {part} 를 안 씀" + support = _src("B03_FileInput", ".ts")["B03_FileInput_UI_Support.ts"] + assert "