Merge remote-tracking branches 'origin/sub_laptop_1', 'origin/sub_laptop_3' and 'origin/sub_laptop_5' into main_laptop_1
This commit is contained in:
@@ -5,12 +5,15 @@
|
||||
* 제약 준수:
|
||||
* - 모든 문구는 ui_template_locale.ts 참조 (하드코딩 금지).
|
||||
* - 색상/간격은 A01_Home_UI_Style.css + theme.css 변수만 사용.
|
||||
* - 공통 컴포넌트(createButton/createCard/createTag) 우선 사용.
|
||||
* - 공통 부품(createPublicFrame/createCard/createBadge/createButton) 우선 사용.
|
||||
* - 이벤트 핸들러는 on[페이지]_[기능]_[액션] 명명.
|
||||
* ========================================================================== */
|
||||
|
||||
import { ui_locales, currentLanguageIndex } from "@ui/ui_template_locale";
|
||||
import { createButton, createCard, createTag } from "@ui/ui_template_elements";
|
||||
import { createBadge } from "@ui/ui_template_badge";
|
||||
import { createButton, createCard } from "@ui/ui_template_elements";
|
||||
import { createPublicFrame } from "@ui/ui_template_public_layout";
|
||||
import { createText } from "@ui/ui_template_text";
|
||||
import { navigateTo } from "../A00_Common/router";
|
||||
import { ROUTES } from "@config/config_frontend";
|
||||
import "./A01_Home_UI_Style.css";
|
||||
@@ -49,57 +52,12 @@ function onA01_Home_Explore_Click(): void {
|
||||
/* -----------------------------------------------------------------------------
|
||||
* 섹션 빌더
|
||||
* -------------------------------------------------------------------------- */
|
||||
function buildHero(): HTMLElement {
|
||||
const hero = document.createElement("section");
|
||||
hero.className = "a01-hero";
|
||||
|
||||
const inner = document.createElement("div");
|
||||
inner.className = "a01-hero__inner";
|
||||
|
||||
const title = document.createElement("h1");
|
||||
title.className = "a01-hero__title";
|
||||
title.textContent = L("A01_Home_Title");
|
||||
|
||||
const subtitle = document.createElement("p");
|
||||
subtitle.className = "a01-hero__subtitle";
|
||||
subtitle.textContent = L("A01_Home_Hero_Subtitle");
|
||||
|
||||
const ctas = document.createElement("div");
|
||||
ctas.className = "a01-hero__ctas";
|
||||
ctas.append(
|
||||
createButton({
|
||||
label: L("A01_Home_Hero_CtaPrimary"),
|
||||
variant: "filled",
|
||||
onClick: onA01_Home_GetStarted_Click,
|
||||
}),
|
||||
createButton({
|
||||
label: L("A01_Home_Hero_CtaSecondary"),
|
||||
variant: "ghost",
|
||||
onClick: onA01_Home_Explore_Click,
|
||||
}),
|
||||
);
|
||||
|
||||
inner.append(title, subtitle, ctas);
|
||||
hero.append(inner);
|
||||
return hero;
|
||||
}
|
||||
|
||||
function buildNews(): HTMLElement {
|
||||
const section = document.createElement("section");
|
||||
section.className = "a01-section";
|
||||
|
||||
const heading = document.createElement("h2");
|
||||
heading.className = "a01-section__title";
|
||||
heading.textContent = L("A01_Home_News_SectionTitle");
|
||||
|
||||
const list = document.createElement("div");
|
||||
list.className = "a01-news-list";
|
||||
|
||||
for (const item of MOCK_NEWS) {
|
||||
const row = document.createElement("div");
|
||||
row.className = "a01-news-row";
|
||||
|
||||
const tag = createTag(L("A01_Home_News_Tag"), "accent");
|
||||
const tag = createBadge(L("A01_Home_News_Tag"), "accent");
|
||||
const titleEl = document.createElement("span");
|
||||
titleEl.className = "a01-news-row__title";
|
||||
titleEl.textContent = item.title;
|
||||
@@ -107,22 +65,14 @@ function buildNews(): HTMLElement {
|
||||
dateEl.className = "a01-news-row__date";
|
||||
dateEl.textContent = item.date;
|
||||
|
||||
row.append(tag, titleEl, dateEl);
|
||||
const row = createCard({ body: [tag, titleEl, dateEl] });
|
||||
row.classList.add("a01-news-row");
|
||||
list.append(row);
|
||||
}
|
||||
|
||||
section.append(heading, list);
|
||||
return section;
|
||||
return list;
|
||||
}
|
||||
|
||||
function buildFeatures(): HTMLElement {
|
||||
const section = document.createElement("section");
|
||||
section.className = "a01-section";
|
||||
|
||||
const heading = document.createElement("h2");
|
||||
heading.className = "a01-section__title";
|
||||
heading.textContent = L("A01_Home_Features_SectionTitle");
|
||||
|
||||
const grid = document.createElement("div");
|
||||
grid.className = "a01-feature-grid";
|
||||
|
||||
@@ -133,24 +83,34 @@ function buildFeatures(): HTMLElement {
|
||||
];
|
||||
|
||||
for (const [titleKey, descKey] of features) {
|
||||
const desc = document.createElement("p");
|
||||
desc.className = "a01-feature__desc";
|
||||
desc.textContent = L(descKey);
|
||||
const card = createCard({ title: L(titleKey), body: [desc] });
|
||||
const card = createCard({ title: L(titleKey), body: [createText(L(descKey), "muted", 14)] });
|
||||
card.classList.add("a01-feature-card");
|
||||
grid.append(card);
|
||||
}
|
||||
|
||||
section.append(heading, grid);
|
||||
return section;
|
||||
return grid;
|
||||
}
|
||||
|
||||
/* -----------------------------------------------------------------------------
|
||||
* 페이지 진입점
|
||||
* -------------------------------------------------------------------------- */
|
||||
export function renderA01Home(root: HTMLElement): void {
|
||||
const page = document.createElement("div");
|
||||
page.className = "a01-home";
|
||||
page.append(buildHero(), buildNews(), buildFeatures());
|
||||
root.append(page);
|
||||
const frame = createPublicFrame({
|
||||
title: L("A01_Home_Title"),
|
||||
lead: L("A01_Home_Hero_Subtitle"),
|
||||
actions: [
|
||||
createButton({
|
||||
label: L("A01_Home_Hero_CtaPrimary"),
|
||||
variant: "filled",
|
||||
onClick: onA01_Home_GetStarted_Click,
|
||||
}),
|
||||
createButton({
|
||||
label: L("A01_Home_Hero_CtaSecondary"),
|
||||
variant: "ghost",
|
||||
onClick: onA01_Home_Explore_Click,
|
||||
}),
|
||||
],
|
||||
});
|
||||
frame.addSection(L("A01_Home_News_SectionTitle")).append(buildNews());
|
||||
frame.addSection(L("A01_Home_Features_SectionTitle")).append(buildFeatures());
|
||||
root.append(frame.root);
|
||||
}
|
||||
|
||||
@@ -1,66 +1,8 @@
|
||||
/* =============================================================================
|
||||
* A01_Home_UI_Style.css
|
||||
* 홈 페이지 고유 스타일 — theme.css 변수만 참조 (하드코딩 금지).
|
||||
* 홈 페이지 고유 배치 — 머리 · 구역 · 카드 겉은 공용 부품(공개 화면 틀 · 카드)이 냄.
|
||||
* ========================================================================== */
|
||||
|
||||
.a01-home {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--section-gap);
|
||||
padding-bottom: var(--spacing-64);
|
||||
}
|
||||
|
||||
/* --- Hero --- */
|
||||
.a01-hero {
|
||||
/* 라벤더 그라디언트는 흰 표면 아래에만: 히어로 배경으로 사용 */
|
||||
background: linear-gradient(to bottom, var(--color-mist-violet), var(--color-canvas));
|
||||
padding: var(--spacing-96) var(--spacing-32);
|
||||
}
|
||||
.a01-hero__inner {
|
||||
max-width: var(--page-max-width);
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
gap: var(--spacing-24);
|
||||
}
|
||||
.a01-hero__title {
|
||||
font-size: var(--text-heading-lg);
|
||||
line-height: var(--leading-heading-lg);
|
||||
color: var(--color-deep-iris);
|
||||
max-width: 14ch;
|
||||
}
|
||||
.a01-hero__subtitle {
|
||||
font-size: var(--text-body);
|
||||
line-height: var(--leading-body);
|
||||
color: var(--color-slate);
|
||||
max-width: 52ch;
|
||||
margin: 0;
|
||||
}
|
||||
.a01-hero__ctas {
|
||||
display: flex;
|
||||
gap: var(--spacing-16);
|
||||
margin-top: var(--spacing-8);
|
||||
}
|
||||
|
||||
/* --- 공통 섹션 --- */
|
||||
.a01-section {
|
||||
max-width: var(--page-max-width);
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
padding: 0 var(--spacing-32);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-24);
|
||||
}
|
||||
.a01-section__title {
|
||||
font-size: var(--text-heading-sm);
|
||||
line-height: var(--leading-heading-sm);
|
||||
color: var(--color-deep-iris);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* --- News --- */
|
||||
.a01-news-list {
|
||||
display: flex;
|
||||
@@ -68,21 +10,20 @@
|
||||
gap: var(--spacing-8);
|
||||
}
|
||||
.a01-news-row {
|
||||
display: flex;
|
||||
padding: var(--spacing-16) var(--spacing-24);
|
||||
}
|
||||
.a01-news-row > .ui-card__body {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: var(--spacing-16);
|
||||
padding: var(--spacing-16) var(--spacing-24);
|
||||
background-color: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-cards);
|
||||
}
|
||||
.a01-news-row__title {
|
||||
flex: 1;
|
||||
color: var(--color-charcoal);
|
||||
color: var(--color-text);
|
||||
font-size: var(--text-body-sm);
|
||||
}
|
||||
.a01-news-row__date {
|
||||
color: var(--color-ash);
|
||||
color: var(--color-text-muted);
|
||||
font-size: var(--text-caption);
|
||||
}
|
||||
|
||||
@@ -95,18 +36,9 @@
|
||||
.a01-feature-card {
|
||||
min-height: 140px;
|
||||
}
|
||||
.a01-feature__desc {
|
||||
margin: 0;
|
||||
color: var(--color-slate);
|
||||
font-size: var(--text-body-sm);
|
||||
line-height: var(--leading-body-sm);
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.a01-feature-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.a01-hero__title {
|
||||
font-size: var(--text-heading);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,12 +5,14 @@
|
||||
* 제약 준수:
|
||||
* - 모든 문구는 ui_template_locale.ts 참조 (하드코딩 금지).
|
||||
* - 색상/간격은 A02_ProgDetail_UI_Style.css + theme.css 변수만 사용.
|
||||
* - 공통 컴포넌트(createButton/createCard) 우선 사용.
|
||||
* - 공통 부품(createPublicFrame/createCard/createButton) 우선 사용.
|
||||
* - 이벤트 핸들러는 on[페이지]_[기능]_[액션] 명명.
|
||||
* ========================================================================== */
|
||||
|
||||
import { ui_locales, currentLanguageIndex } from "@ui/ui_template_locale";
|
||||
import { createButton, createCard } from "@ui/ui_template_elements";
|
||||
import { createPublicFrame } from "@ui/ui_template_public_layout";
|
||||
import { createText } from "@ui/ui_template_text";
|
||||
import { navigateTo } from "../A00_Common/router";
|
||||
import { ROUTES } from "@config/config_frontend";
|
||||
import "./A02_ProgDetail_UI_Style.css";
|
||||
@@ -30,40 +32,7 @@ function onA02_ProgDetail_Start_Click(): void {
|
||||
/* -----------------------------------------------------------------------------
|
||||
* 섹션 빌더
|
||||
* -------------------------------------------------------------------------- */
|
||||
function buildHero(): HTMLElement {
|
||||
const hero = document.createElement("section");
|
||||
hero.className = "a02-hero";
|
||||
|
||||
const inner = document.createElement("div");
|
||||
inner.className = "a02-hero__inner";
|
||||
|
||||
const title = document.createElement("h1");
|
||||
title.className = "a02-hero__title";
|
||||
title.textContent = L("A02_ProgDetail_Title");
|
||||
|
||||
const subtitle = document.createElement("p");
|
||||
subtitle.className = "a02-hero__subtitle";
|
||||
subtitle.textContent = L("A02_ProgDetail_Hero_Subtitle");
|
||||
|
||||
const cta = createButton({
|
||||
label: L("A02_ProgDetail_Hero_Cta"),
|
||||
variant: "filled",
|
||||
onClick: onA02_ProgDetail_Start_Click,
|
||||
});
|
||||
|
||||
inner.append(title, subtitle, cta);
|
||||
hero.append(inner);
|
||||
return hero;
|
||||
}
|
||||
|
||||
function buildWorkflow(): HTMLElement {
|
||||
const section = document.createElement("section");
|
||||
section.className = "a02-section";
|
||||
|
||||
const heading = document.createElement("h2");
|
||||
heading.className = "a02-section__title";
|
||||
heading.textContent = L("A02_ProgDetail_Workflow_SectionTitle");
|
||||
|
||||
const list = document.createElement("div");
|
||||
list.className = "a02-workflow-list";
|
||||
|
||||
@@ -77,9 +46,6 @@ function buildWorkflow(): HTMLElement {
|
||||
];
|
||||
|
||||
steps.forEach(([titleKey, descKey], index) => {
|
||||
const row = document.createElement("div");
|
||||
row.className = "a02-workflow-row";
|
||||
|
||||
const badge = document.createElement("span");
|
||||
badge.className = "a02-workflow-row__badge";
|
||||
badge.textContent = String(index + 1);
|
||||
@@ -91,27 +57,15 @@ function buildWorkflow(): HTMLElement {
|
||||
stepTitle.className = "a02-workflow-row__title";
|
||||
stepTitle.textContent = L(titleKey);
|
||||
|
||||
const stepDesc = document.createElement("p");
|
||||
stepDesc.className = "a02-workflow-row__desc";
|
||||
stepDesc.textContent = L(descKey);
|
||||
|
||||
text.append(stepTitle, stepDesc);
|
||||
row.append(badge, text);
|
||||
text.append(stepTitle, createText(L(descKey), "muted", 14));
|
||||
const row = createCard({ body: [badge, text] });
|
||||
row.classList.add("a02-workflow-row");
|
||||
list.append(row);
|
||||
});
|
||||
|
||||
section.append(heading, list);
|
||||
return section;
|
||||
return list;
|
||||
}
|
||||
|
||||
function buildTech(): HTMLElement {
|
||||
const section = document.createElement("section");
|
||||
section.className = "a02-section";
|
||||
|
||||
const heading = document.createElement("h2");
|
||||
heading.className = "a02-section__title";
|
||||
heading.textContent = L("A02_ProgDetail_Tech_SectionTitle");
|
||||
|
||||
const grid = document.createElement("div");
|
||||
grid.className = "a02-tech-grid";
|
||||
|
||||
@@ -122,24 +76,31 @@ function buildTech(): HTMLElement {
|
||||
];
|
||||
|
||||
for (const [titleKey, descKey] of techs) {
|
||||
const desc = document.createElement("p");
|
||||
desc.className = "a02-tech__desc";
|
||||
desc.textContent = L(descKey);
|
||||
const card = createCard({ title: L(titleKey), body: [desc] });
|
||||
const card = createCard({ title: L(titleKey), body: [createText(L(descKey), "muted", 14)] });
|
||||
card.classList.add("a02-tech-card");
|
||||
grid.append(card);
|
||||
}
|
||||
|
||||
section.append(heading, grid);
|
||||
return section;
|
||||
return grid;
|
||||
}
|
||||
|
||||
/* -----------------------------------------------------------------------------
|
||||
* 페이지 진입점
|
||||
* -------------------------------------------------------------------------- */
|
||||
export function renderA02ProgDetail(root: HTMLElement): void {
|
||||
const page = document.createElement("div");
|
||||
page.className = "a02-prog-detail";
|
||||
page.append(buildHero(), buildWorkflow(), buildTech());
|
||||
root.append(page);
|
||||
const frame = createPublicFrame({
|
||||
title: L("A02_ProgDetail_Title"),
|
||||
lead: L("A02_ProgDetail_Hero_Subtitle"),
|
||||
actions: [
|
||||
createButton({
|
||||
label: L("A02_ProgDetail_Hero_Cta"),
|
||||
variant: "filled",
|
||||
onClick: onA02_ProgDetail_Start_Click,
|
||||
}),
|
||||
],
|
||||
wide: true,
|
||||
});
|
||||
frame.root.classList.add("a02-prog-detail");
|
||||
frame.addSection(L("A02_ProgDetail_Workflow_SectionTitle")).append(buildWorkflow());
|
||||
frame.addSection(L("A02_ProgDetail_Tech_SectionTitle")).append(buildTech());
|
||||
root.append(frame.root);
|
||||
}
|
||||
|
||||
@@ -1,58 +1,11 @@
|
||||
/* =============================================================================
|
||||
* A02_ProgDetail_UI_Style.css
|
||||
* 프로그램 상세 페이지 고유 스타일 — theme.css 변수만 참조 (하드코딩 금지).
|
||||
* 프로그램 상세 페이지 고유 배치 — 머리 · 구역 · 카드 겉은 공용 부품(공개 화면 틀 · 카드)이 냄.
|
||||
* ========================================================================== */
|
||||
|
||||
.a02-prog-detail {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--section-gap);
|
||||
padding-bottom: var(--spacing-64);
|
||||
}
|
||||
|
||||
/* --- Hero --- */
|
||||
.a02-hero {
|
||||
background: linear-gradient(to bottom, var(--color-mist-violet), var(--color-canvas));
|
||||
padding: var(--spacing-96) var(--spacing-32);
|
||||
}
|
||||
.a02-hero__inner {
|
||||
max-width: var(--page-max-width);
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
gap: var(--spacing-24);
|
||||
}
|
||||
.a02-hero__title {
|
||||
font-size: var(--text-heading-lg);
|
||||
line-height: var(--leading-heading-lg);
|
||||
color: var(--color-deep-iris);
|
||||
max-width: 18ch;
|
||||
}
|
||||
.a02-hero__subtitle {
|
||||
font-size: var(--text-body);
|
||||
line-height: var(--leading-body);
|
||||
color: var(--color-slate);
|
||||
max-width: 56ch;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* --- 공통 섹션 --- */
|
||||
.a02-section {
|
||||
max-width: var(--page-max-width);
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
padding: 0 var(--spacing-32);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-24);
|
||||
}
|
||||
.a02-section__title {
|
||||
font-size: var(--text-heading-sm);
|
||||
line-height: var(--leading-heading-sm);
|
||||
color: var(--color-deep-iris);
|
||||
text-align: center;
|
||||
/* 단추 하나뿐인 머리 — 단추 줄 위 여백 없음 */
|
||||
.a02-prog-detail .ui-public-frame__actions {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
/* --- Workflow (번호 배지 + 텍스트) --- */
|
||||
@@ -61,14 +14,10 @@
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-16);
|
||||
}
|
||||
.a02-workflow-row {
|
||||
display: flex;
|
||||
.a02-workflow-row > .ui-card__body {
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
gap: var(--spacing-24);
|
||||
padding: var(--spacing-24);
|
||||
background-color: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-cards);
|
||||
}
|
||||
.a02-workflow-row__badge {
|
||||
flex-shrink: 0;
|
||||
@@ -90,15 +39,9 @@
|
||||
}
|
||||
.a02-workflow-row__title {
|
||||
font-size: var(--text-body);
|
||||
color: var(--color-charcoal);
|
||||
color: var(--color-text);
|
||||
margin: 0;
|
||||
}
|
||||
.a02-workflow-row__desc {
|
||||
margin: 0;
|
||||
color: var(--color-slate);
|
||||
font-size: var(--text-body-sm);
|
||||
line-height: var(--leading-body-sm);
|
||||
}
|
||||
|
||||
/* --- Tech --- */
|
||||
.a02-tech-grid {
|
||||
@@ -109,18 +52,9 @@
|
||||
.a02-tech-card {
|
||||
min-height: 140px;
|
||||
}
|
||||
.a02-tech__desc {
|
||||
margin: 0;
|
||||
color: var(--color-slate);
|
||||
font-size: var(--text-body-sm);
|
||||
line-height: var(--leading-body-sm);
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.a02-tech-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.a02-hero__title {
|
||||
font-size: var(--text-heading);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,11 +5,13 @@
|
||||
* 제약 준수:
|
||||
* - 모든 문구는 ui_template_locale.ts 참조 (하드코딩 금지).
|
||||
* - 색상/간격은 A03_CompDetail_UI_Style.css + theme.css 변수만 사용.
|
||||
* - 공통 컴포넌트(createCard) 우선 사용.
|
||||
* - 공통 부품(createPublicFrame/createCard) 우선 사용.
|
||||
* ========================================================================== */
|
||||
|
||||
import { ui_locales, currentLanguageIndex } from "@ui/ui_template_locale";
|
||||
import { createCard } from "@ui/ui_template_elements";
|
||||
import { createPublicFrame } from "@ui/ui_template_public_layout";
|
||||
import { createText } from "@ui/ui_template_text";
|
||||
import "./A03_CompDetail_UI_Style.css";
|
||||
|
||||
/** locale 헬퍼 */
|
||||
@@ -20,50 +22,14 @@ function L(key: keyof typeof ui_locales): string {
|
||||
/* -----------------------------------------------------------------------------
|
||||
* 섹션 빌더
|
||||
* -------------------------------------------------------------------------- */
|
||||
function buildHero(): HTMLElement {
|
||||
const hero = document.createElement("section");
|
||||
hero.className = "a03-hero";
|
||||
|
||||
const inner = document.createElement("div");
|
||||
inner.className = "a03-hero__inner";
|
||||
|
||||
const title = document.createElement("h1");
|
||||
title.className = "a03-hero__title";
|
||||
title.textContent = L("A03_CompDetail_Title");
|
||||
|
||||
const subtitle = document.createElement("p");
|
||||
subtitle.className = "a03-hero__subtitle";
|
||||
subtitle.textContent = L("A03_CompDetail_Hero_Subtitle");
|
||||
|
||||
inner.append(title, subtitle);
|
||||
hero.append(inner);
|
||||
return hero;
|
||||
}
|
||||
|
||||
function buildMission(): HTMLElement {
|
||||
const section = document.createElement("section");
|
||||
section.className = "a03-section";
|
||||
|
||||
const heading = document.createElement("h2");
|
||||
heading.className = "a03-section__title";
|
||||
heading.textContent = L("A03_CompDetail_Mission_SectionTitle");
|
||||
|
||||
const body = document.createElement("p");
|
||||
body.className = "a03-mission__body";
|
||||
body.textContent = L("A03_CompDetail_Mission_Body");
|
||||
|
||||
section.append(heading, body);
|
||||
return section;
|
||||
return body;
|
||||
}
|
||||
|
||||
function buildValues(): HTMLElement {
|
||||
const section = document.createElement("section");
|
||||
section.className = "a03-section";
|
||||
|
||||
const heading = document.createElement("h2");
|
||||
heading.className = "a03-section__title";
|
||||
heading.textContent = L("A03_CompDetail_Value_SectionTitle");
|
||||
|
||||
const grid = document.createElement("div");
|
||||
grid.className = "a03-value-grid";
|
||||
|
||||
@@ -74,40 +40,31 @@ function buildValues(): HTMLElement {
|
||||
];
|
||||
|
||||
for (const [titleKey, descKey] of values) {
|
||||
const desc = document.createElement("p");
|
||||
desc.className = "a03-value__desc";
|
||||
desc.textContent = L(descKey);
|
||||
const card = createCard({ title: L(titleKey), body: [desc] });
|
||||
const card = createCard({ title: L(titleKey), body: [createText(L(descKey), "muted", 14)] });
|
||||
card.classList.add("a03-value-card");
|
||||
grid.append(card);
|
||||
}
|
||||
|
||||
section.append(heading, grid);
|
||||
return section;
|
||||
return grid;
|
||||
}
|
||||
|
||||
function buildContact(): HTMLElement {
|
||||
const section = document.createElement("section");
|
||||
section.className = "a03-section";
|
||||
|
||||
const heading = document.createElement("h2");
|
||||
heading.className = "a03-section__title";
|
||||
heading.textContent = L("A03_CompDetail_Contact_SectionTitle");
|
||||
|
||||
const email = document.createElement("p");
|
||||
email.className = "a03-contact__email";
|
||||
email.textContent = L("A03_CompDetail_Contact_Email");
|
||||
|
||||
section.append(heading, email);
|
||||
return section;
|
||||
return email;
|
||||
}
|
||||
|
||||
/* -----------------------------------------------------------------------------
|
||||
* 페이지 진입점
|
||||
* -------------------------------------------------------------------------- */
|
||||
export function renderA03CompDetail(root: HTMLElement): void {
|
||||
const page = document.createElement("div");
|
||||
page.className = "a03-comp-detail";
|
||||
page.append(buildHero(), buildMission(), buildValues(), buildContact());
|
||||
root.append(page);
|
||||
const frame = createPublicFrame({
|
||||
title: L("A03_CompDetail_Title"),
|
||||
lead: L("A03_CompDetail_Hero_Subtitle"),
|
||||
wide: true,
|
||||
});
|
||||
frame.addSection(L("A03_CompDetail_Mission_SectionTitle")).append(buildMission());
|
||||
frame.addSection(L("A03_CompDetail_Value_SectionTitle")).append(buildValues());
|
||||
frame.addSection(L("A03_CompDetail_Contact_SectionTitle")).append(buildContact());
|
||||
root.append(frame.root);
|
||||
}
|
||||
|
||||
@@ -1,66 +1,14 @@
|
||||
/* =============================================================================
|
||||
* A03_CompDetail_UI_Style.css
|
||||
* 회사 상세 페이지 고유 스타일 — theme.css 변수만 참조 (하드코딩 금지).
|
||||
* 회사 상세 페이지 고유 배치 — 머리 · 구역 · 카드 겉은 공용 부품(공개 화면 틀 · 카드)이 냄.
|
||||
* ========================================================================== */
|
||||
|
||||
.a03-comp-detail {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--section-gap);
|
||||
padding-bottom: var(--spacing-64);
|
||||
}
|
||||
|
||||
/* --- Hero --- */
|
||||
.a03-hero {
|
||||
background: linear-gradient(to bottom, var(--color-mist-violet), var(--color-canvas));
|
||||
padding: var(--spacing-96) var(--spacing-32);
|
||||
}
|
||||
.a03-hero__inner {
|
||||
max-width: var(--page-max-width);
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
gap: var(--spacing-24);
|
||||
}
|
||||
.a03-hero__title {
|
||||
font-size: var(--text-heading-lg);
|
||||
line-height: var(--leading-heading-lg);
|
||||
color: var(--color-deep-iris);
|
||||
max-width: 18ch;
|
||||
}
|
||||
.a03-hero__subtitle {
|
||||
font-size: var(--text-body);
|
||||
line-height: var(--leading-body);
|
||||
color: var(--color-slate);
|
||||
max-width: 56ch;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* --- 공통 섹션 --- */
|
||||
.a03-section {
|
||||
max-width: var(--page-max-width);
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
padding: 0 var(--spacing-32);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-24);
|
||||
}
|
||||
.a03-section__title {
|
||||
font-size: var(--text-heading-sm);
|
||||
line-height: var(--leading-heading-sm);
|
||||
color: var(--color-deep-iris);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* --- Mission --- */
|
||||
.a03-mission__body {
|
||||
margin: 0 auto;
|
||||
max-width: 60ch;
|
||||
text-align: center;
|
||||
color: var(--color-slate);
|
||||
color: var(--color-text-secondary);
|
||||
font-size: var(--text-body);
|
||||
line-height: var(--leading-body);
|
||||
}
|
||||
@@ -74,18 +22,12 @@
|
||||
.a03-value-card {
|
||||
min-height: 140px;
|
||||
}
|
||||
.a03-value__desc {
|
||||
margin: 0;
|
||||
color: var(--color-slate);
|
||||
font-size: var(--text-body-sm);
|
||||
line-height: var(--leading-body-sm);
|
||||
}
|
||||
|
||||
/* --- Contact --- */
|
||||
.a03-contact__email {
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
color: var(--color-charcoal);
|
||||
color: var(--color-text);
|
||||
font-size: var(--text-body);
|
||||
}
|
||||
|
||||
@@ -93,7 +35,4 @@
|
||||
.a03-value-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.a03-hero__title {
|
||||
font-size: var(--text-heading);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,12 +5,15 @@
|
||||
* 제약 준수:
|
||||
* - 모든 문구는 ui_template_locale.ts 참조 (하드코딩 금지).
|
||||
* - 색상/간격은 A04_NewsHistory_UI_Style.css + theme.css 변수만 사용.
|
||||
* - 공통 컴포넌트(createTag) 우선 사용.
|
||||
* - 공통 부품(createPublicFrame/createCard/createBadge) 우선 사용.
|
||||
* - Mock 데이터는 나중에 GET /api/a04/history로 교체.
|
||||
* ========================================================================== */
|
||||
|
||||
import { ui_locales, currentLanguageIndex } from "@ui/ui_template_locale";
|
||||
import { createTag, type TagVariant } from "@ui/ui_template_elements";
|
||||
import { createBadge, type BadgeTone } from "@ui/ui_template_badge";
|
||||
import { createCard } from "@ui/ui_template_elements";
|
||||
import { createEmpty } from "@ui/ui_template_empty";
|
||||
import { createPublicFrame } from "@ui/ui_template_public_layout";
|
||||
import "./A04_NewsHistory_UI_Style.css";
|
||||
|
||||
/** locale 헬퍼 */
|
||||
@@ -38,7 +41,7 @@ const MOCK_HISTORY: HistoryItem[] = [
|
||||
];
|
||||
|
||||
/** 이력 종류별 태그 변형/라벨 매핑 */
|
||||
const KIND_META: Record<HistoryKind, { variant: TagVariant; labelKey: keyof typeof ui_locales }> = {
|
||||
const KIND_META: Record<HistoryKind, { variant: BadgeTone; labelKey: keyof typeof ui_locales }> = {
|
||||
feature: { variant: "accent", labelKey: "A04_NewsHistory_Tag_Feature" },
|
||||
fix: { variant: "success", labelKey: "A04_NewsHistory_Tag_Fix" },
|
||||
notice: { variant: "neutral", labelKey: "A04_NewsHistory_Tag_Notice" },
|
||||
@@ -47,47 +50,14 @@ const KIND_META: Record<HistoryKind, { variant: TagVariant; labelKey: keyof type
|
||||
/* -----------------------------------------------------------------------------
|
||||
* 섹션 빌더
|
||||
* -------------------------------------------------------------------------- */
|
||||
function buildHero(): HTMLElement {
|
||||
const hero = document.createElement("section");
|
||||
hero.className = "a04-hero";
|
||||
|
||||
const inner = document.createElement("div");
|
||||
inner.className = "a04-hero__inner";
|
||||
|
||||
const title = document.createElement("h1");
|
||||
title.className = "a04-hero__title";
|
||||
title.textContent = L("A04_NewsHistory_Title");
|
||||
|
||||
const subtitle = document.createElement("p");
|
||||
subtitle.className = "a04-hero__subtitle";
|
||||
subtitle.textContent = L("A04_NewsHistory_Hero_Subtitle");
|
||||
|
||||
inner.append(title, subtitle);
|
||||
hero.append(inner);
|
||||
return hero;
|
||||
}
|
||||
|
||||
function buildTimeline(): HTMLElement {
|
||||
const section = document.createElement("section");
|
||||
section.className = "a04-section";
|
||||
if (MOCK_HISTORY.length === 0) return createEmpty({ text: L("A04_NewsHistory_Empty") });
|
||||
|
||||
const list = document.createElement("div");
|
||||
list.className = "a04-timeline";
|
||||
|
||||
if (MOCK_HISTORY.length === 0) {
|
||||
const empty = document.createElement("p");
|
||||
empty.className = "a04-timeline__empty";
|
||||
empty.textContent = L("A04_NewsHistory_Empty");
|
||||
section.append(empty);
|
||||
return section;
|
||||
}
|
||||
|
||||
for (const item of MOCK_HISTORY) {
|
||||
const meta = KIND_META[item.kind];
|
||||
const row = document.createElement("div");
|
||||
row.className = "a04-timeline-row";
|
||||
|
||||
const tag = createTag(L(meta.labelKey), meta.variant);
|
||||
const tag = createBadge(L(meta.labelKey), meta.variant);
|
||||
|
||||
const titleEl = document.createElement("span");
|
||||
titleEl.className = "a04-timeline-row__title";
|
||||
@@ -97,20 +67,22 @@ function buildTimeline(): HTMLElement {
|
||||
dateEl.className = "a04-timeline-row__date";
|
||||
dateEl.textContent = item.date;
|
||||
|
||||
row.append(tag, titleEl, dateEl);
|
||||
const row = createCard({ body: [tag, titleEl, dateEl] });
|
||||
row.classList.add("a04-timeline-row");
|
||||
list.append(row);
|
||||
}
|
||||
|
||||
section.append(list);
|
||||
return section;
|
||||
return list;
|
||||
}
|
||||
|
||||
/* -----------------------------------------------------------------------------
|
||||
* 페이지 진입점
|
||||
* -------------------------------------------------------------------------- */
|
||||
export function renderA04NewsHistory(root: HTMLElement): void {
|
||||
const page = document.createElement("div");
|
||||
page.className = "a04-news-history";
|
||||
page.append(buildHero(), buildTimeline());
|
||||
root.append(page);
|
||||
const frame = createPublicFrame({
|
||||
title: L("A04_NewsHistory_Title"),
|
||||
lead: L("A04_NewsHistory_Hero_Subtitle"),
|
||||
wide: true,
|
||||
});
|
||||
frame.addSection().append(buildTimeline());
|
||||
root.append(frame.root);
|
||||
}
|
||||
|
||||
@@ -1,54 +1,8 @@
|
||||
/* =============================================================================
|
||||
* A04_NewsHistory_UI_Style.css
|
||||
* 최신소식/개선이력 페이지 고유 스타일 — theme.css 변수만 참조 (하드코딩 금지).
|
||||
* 최신소식 · 개선 이력 고유 배치 — 머리 · 구역 · 카드 겉은 공용 부품(공개 화면 틀 · 카드)이 냄.
|
||||
* ========================================================================== */
|
||||
|
||||
.a04-news-history {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--section-gap);
|
||||
padding-bottom: var(--spacing-64);
|
||||
}
|
||||
|
||||
/* --- Hero --- */
|
||||
.a04-hero {
|
||||
background: linear-gradient(to bottom, var(--color-mist-violet), var(--color-canvas));
|
||||
padding: var(--spacing-96) var(--spacing-32);
|
||||
}
|
||||
.a04-hero__inner {
|
||||
max-width: var(--page-max-width);
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
gap: var(--spacing-24);
|
||||
}
|
||||
.a04-hero__title {
|
||||
font-size: var(--text-heading-lg);
|
||||
line-height: var(--leading-heading-lg);
|
||||
color: var(--color-deep-iris);
|
||||
max-width: 18ch;
|
||||
}
|
||||
.a04-hero__subtitle {
|
||||
font-size: var(--text-body);
|
||||
line-height: var(--leading-body);
|
||||
color: var(--color-slate);
|
||||
max-width: 56ch;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* --- 공통 섹션 --- */
|
||||
.a04-section {
|
||||
max-width: var(--page-max-width);
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
padding: 0 var(--spacing-32);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-24);
|
||||
}
|
||||
|
||||
/* --- Timeline --- */
|
||||
.a04-timeline {
|
||||
display: flex;
|
||||
@@ -56,31 +10,19 @@
|
||||
gap: var(--spacing-8);
|
||||
}
|
||||
.a04-timeline-row {
|
||||
display: flex;
|
||||
padding: var(--spacing-16) var(--spacing-24);
|
||||
}
|
||||
.a04-timeline-row > .ui-card__body {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: var(--spacing-16);
|
||||
padding: var(--spacing-16) var(--spacing-24);
|
||||
background-color: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-cards);
|
||||
}
|
||||
.a04-timeline-row__title {
|
||||
flex: 1;
|
||||
color: var(--color-charcoal);
|
||||
color: var(--color-text);
|
||||
font-size: var(--text-body-sm);
|
||||
}
|
||||
.a04-timeline-row__date {
|
||||
color: var(--color-ash);
|
||||
color: var(--color-text-muted);
|
||||
font-size: var(--text-caption);
|
||||
}
|
||||
.a04-timeline__empty {
|
||||
text-align: center;
|
||||
color: var(--color-slate);
|
||||
font-size: var(--text-body);
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.a04-hero__title {
|
||||
font-size: var(--text-heading);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,12 +5,14 @@
|
||||
* 제약 준수:
|
||||
* - 모든 문구는 ui_template_locale.ts 참조 (하드코딩 금지).
|
||||
* - 색상/간격은 A05_EduDetail_UI_Style.css + theme.css 변수만 사용.
|
||||
* - 공통 컴포넌트(createButton/createCard) 우선 사용.
|
||||
* - 공통 부품(createPublicFrame/createCard/createButton) 우선 사용.
|
||||
* - 이벤트 핸들러는 on[페이지]_[기능]_[액션] 명명.
|
||||
* ========================================================================== */
|
||||
|
||||
import { ui_locales, currentLanguageIndex } from "@ui/ui_template_locale";
|
||||
import { createButton, createCard } from "@ui/ui_template_elements";
|
||||
import { createPublicFrame } from "@ui/ui_template_public_layout";
|
||||
import { createText } from "@ui/ui_template_text";
|
||||
import { navigateTo } from "../A00_Common/router";
|
||||
import { ROUTES } from "@config/config_frontend";
|
||||
import "./A05_EduDetail_UI_Style.css";
|
||||
@@ -30,34 +32,7 @@ function onA05_EduDetail_Inquiry_Click(): void {
|
||||
/* -----------------------------------------------------------------------------
|
||||
* 섹션 빌더
|
||||
* -------------------------------------------------------------------------- */
|
||||
function buildHero(): HTMLElement {
|
||||
const hero = document.createElement("section");
|
||||
hero.className = "a05-hero";
|
||||
|
||||
const inner = document.createElement("div");
|
||||
inner.className = "a05-hero__inner";
|
||||
|
||||
const title = document.createElement("h1");
|
||||
title.className = "a05-hero__title";
|
||||
title.textContent = L("A05_EduDetail_Title");
|
||||
|
||||
const subtitle = document.createElement("p");
|
||||
subtitle.className = "a05-hero__subtitle";
|
||||
subtitle.textContent = L("A05_EduDetail_Hero_Subtitle");
|
||||
|
||||
inner.append(title, subtitle);
|
||||
hero.append(inner);
|
||||
return hero;
|
||||
}
|
||||
|
||||
function buildCourses(): HTMLElement {
|
||||
const section = document.createElement("section");
|
||||
section.className = "a05-section";
|
||||
|
||||
const heading = document.createElement("h2");
|
||||
heading.className = "a05-section__title";
|
||||
heading.textContent = L("A05_EduDetail_Course_SectionTitle");
|
||||
|
||||
const grid = document.createElement("div");
|
||||
grid.className = "a05-course-grid";
|
||||
|
||||
@@ -68,38 +43,31 @@ function buildCourses(): HTMLElement {
|
||||
];
|
||||
|
||||
for (const [titleKey, descKey] of courses) {
|
||||
const desc = document.createElement("p");
|
||||
desc.className = "a05-course__desc";
|
||||
desc.textContent = L(descKey);
|
||||
const card = createCard({ title: L(titleKey), body: [desc] });
|
||||
const card = createCard({ title: L(titleKey), body: [createText(L(descKey), "muted", 14)] });
|
||||
card.classList.add("a05-course-card");
|
||||
grid.append(card);
|
||||
}
|
||||
|
||||
section.append(heading, grid);
|
||||
return section;
|
||||
}
|
||||
|
||||
function buildCta(): HTMLElement {
|
||||
const section = document.createElement("section");
|
||||
section.className = "a05-section a05-cta";
|
||||
|
||||
section.append(
|
||||
createButton({
|
||||
label: L("A05_EduDetail_Cta"),
|
||||
variant: "filled",
|
||||
onClick: onA05_EduDetail_Inquiry_Click,
|
||||
}),
|
||||
);
|
||||
return section;
|
||||
return grid;
|
||||
}
|
||||
|
||||
/* -----------------------------------------------------------------------------
|
||||
* 페이지 진입점
|
||||
* -------------------------------------------------------------------------- */
|
||||
export function renderA05EduDetail(root: HTMLElement): void {
|
||||
const page = document.createElement("div");
|
||||
page.className = "a05-edu-detail";
|
||||
page.append(buildHero(), buildCourses(), buildCta());
|
||||
root.append(page);
|
||||
const frame = createPublicFrame({
|
||||
title: L("A05_EduDetail_Title"),
|
||||
lead: L("A05_EduDetail_Hero_Subtitle"),
|
||||
wide: true,
|
||||
});
|
||||
frame.addSection(L("A05_EduDetail_Course_SectionTitle")).append(buildCourses());
|
||||
const cta = frame.addSection();
|
||||
cta.classList.add("a05-cta");
|
||||
cta.append(
|
||||
createButton({
|
||||
label: L("A05_EduDetail_Cta"),
|
||||
variant: "filled",
|
||||
onClick: onA05_EduDetail_Inquiry_Click,
|
||||
}),
|
||||
);
|
||||
root.append(frame.root);
|
||||
}
|
||||
|
||||
@@ -1,60 +1,8 @@
|
||||
/* =============================================================================
|
||||
* A05_EduDetail_UI_Style.css
|
||||
* 교육 상세 페이지 고유 스타일 — theme.css 변수만 참조 (하드코딩 금지).
|
||||
* 교육 상세 페이지 고유 배치 — 머리 · 구역 · 카드 겉은 공용 부품(공개 화면 틀 · 카드)이 냄.
|
||||
* ========================================================================== */
|
||||
|
||||
.a05-edu-detail {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--section-gap);
|
||||
padding-bottom: var(--spacing-64);
|
||||
}
|
||||
|
||||
/* --- Hero --- */
|
||||
.a05-hero {
|
||||
background: linear-gradient(to bottom, var(--color-mist-violet), var(--color-canvas));
|
||||
padding: var(--spacing-96) var(--spacing-32);
|
||||
}
|
||||
.a05-hero__inner {
|
||||
max-width: var(--page-max-width);
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
gap: var(--spacing-24);
|
||||
}
|
||||
.a05-hero__title {
|
||||
font-size: var(--text-heading-lg);
|
||||
line-height: var(--leading-heading-lg);
|
||||
color: var(--color-deep-iris);
|
||||
max-width: 18ch;
|
||||
}
|
||||
.a05-hero__subtitle {
|
||||
font-size: var(--text-body);
|
||||
line-height: var(--leading-body);
|
||||
color: var(--color-slate);
|
||||
max-width: 56ch;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* --- 공통 섹션 --- */
|
||||
.a05-section {
|
||||
max-width: var(--page-max-width);
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
padding: 0 var(--spacing-32);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-24);
|
||||
}
|
||||
.a05-section__title {
|
||||
font-size: var(--text-heading-sm);
|
||||
line-height: var(--leading-heading-sm);
|
||||
color: var(--color-deep-iris);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* --- Courses --- */
|
||||
.a05-course-grid {
|
||||
display: grid;
|
||||
@@ -64,12 +12,6 @@
|
||||
.a05-course-card {
|
||||
min-height: 140px;
|
||||
}
|
||||
.a05-course__desc {
|
||||
margin: 0;
|
||||
color: var(--color-slate);
|
||||
font-size: var(--text-body-sm);
|
||||
line-height: var(--leading-body-sm);
|
||||
}
|
||||
|
||||
/* --- CTA --- */
|
||||
.a05-cta {
|
||||
@@ -80,7 +22,4 @@
|
||||
.a05-course-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.a05-hero__title {
|
||||
font-size: var(--text-heading);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -2,6 +2,7 @@ import { ROUTES } from "@config/config_frontend";
|
||||
import { isBlank, isValidEmail } from "@util/common_util_validate";
|
||||
import {
|
||||
createButton,
|
||||
createCard,
|
||||
createInputField,
|
||||
hideLoadingOverlay,
|
||||
showLoadingOverlay,
|
||||
@@ -67,8 +68,6 @@ function clearOtpPending(): void {
|
||||
export function renderA06Login(root: HTMLElement): void {
|
||||
const page = document.createElement("div");
|
||||
page.className = "a06-login";
|
||||
const card = document.createElement("div");
|
||||
card.className = "a06-login__card";
|
||||
const title = document.createElement("h1");
|
||||
title.className = "a06-login__title";
|
||||
title.textContent = L("A06_Login_Title");
|
||||
@@ -215,7 +214,8 @@ export function renderA06Login(root: HTMLElement): void {
|
||||
variant: "ghost",
|
||||
onClick: () => navigateTo(ROUTES.A07_REGISTER),
|
||||
});
|
||||
card.append(title, form, register);
|
||||
const card = createCard({ body: [title, form, register] });
|
||||
card.classList.add("a06-login__card");
|
||||
page.append(card);
|
||||
root.append(page);
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* =============================================================================
|
||||
* A06_Login_UI_Style.css
|
||||
* 로그인 페이지 고유 스타일 — theme.css 변수만 참조 (하드코딩 금지).
|
||||
* 로그인 페이지 고유 배치 — 카드 겉은 공용 부품(카드)이 냄.
|
||||
* ========================================================================== */
|
||||
|
||||
.a06-login {
|
||||
@@ -13,29 +13,16 @@
|
||||
.a06-login__card {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-16);
|
||||
padding: var(--spacing-40);
|
||||
background-color: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-cards);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.a06-login__title {
|
||||
font-size: var(--text-heading-sm);
|
||||
line-height: var(--leading-heading-sm);
|
||||
color: var(--color-deep-iris);
|
||||
color: var(--color-text);
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
}
|
||||
.a06-login__subtitle {
|
||||
font-size: var(--text-body-sm);
|
||||
color: var(--color-slate);
|
||||
text-align: center;
|
||||
margin: 0 0 var(--spacing-8);
|
||||
}
|
||||
|
||||
.a06-login__form {
|
||||
display: flex;
|
||||
@@ -50,10 +37,3 @@
|
||||
.a06-login__otp-actions > .ui-btn {
|
||||
flex: 1;
|
||||
}
|
||||
.a06-login__submit {
|
||||
width: 100%;
|
||||
margin-top: var(--spacing-8);
|
||||
}
|
||||
.a06-login__link {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -2,11 +2,14 @@ import { ROUTES } from "@config/config_frontend";
|
||||
import { hasMinLength, isBlank, isValidEmail } from "@util/common_util_validate";
|
||||
import {
|
||||
createButton,
|
||||
createCard,
|
||||
createInputField,
|
||||
hideLoadingOverlay,
|
||||
showLoadingOverlay,
|
||||
showToast,
|
||||
} from "@ui/ui_template_elements";
|
||||
import { createCheckbox } from "@ui/ui_template_checkbox";
|
||||
import { attachCollapsible } from "@ui/ui_template_collapsible";
|
||||
import { currentLanguageIndex, ui_locales } from "@ui/ui_template_locale";
|
||||
import { navigateTo } from "../A00_Common/router";
|
||||
import { requestRegistration, verifyRegistration } from "./A07_Register_Api_Fetch";
|
||||
@@ -22,38 +25,29 @@ interface AgreementField {
|
||||
|
||||
function checkboxWithAccordion(label: string, contentText: string): AgreementField {
|
||||
const container = document.createElement("div");
|
||||
container.className = "a07-register__agreement";
|
||||
container.className = "ui-collapsible is-collapsed a07-register__agreement";
|
||||
|
||||
const summary = document.createElement("div");
|
||||
summary.className = "a07-register__agreement-summary";
|
||||
summary.className = "ui-collapsible__title a07-register__agreement-summary";
|
||||
|
||||
const input = document.createElement("input");
|
||||
input.type = "checkbox";
|
||||
const box = createCheckbox("");
|
||||
// 체크 상자를 누를 땐 접힘이 바뀌지 않게 — 줄의 나머지를 누르면 약관이 펴짐
|
||||
box.root.addEventListener("click", (e) => e.stopPropagation());
|
||||
|
||||
const labelSpan = document.createElement("span");
|
||||
labelSpan.className = "a07-register__agreement-label";
|
||||
labelSpan.textContent = label;
|
||||
|
||||
summary.append(input, labelSpan);
|
||||
summary.append(box.root, labelSpan);
|
||||
|
||||
const details = document.createElement("details");
|
||||
details.className = "a07-register__agreement-details";
|
||||
const content = document.createElement("div");
|
||||
content.className = "a07-register__agreement-content";
|
||||
content.textContent = contentText;
|
||||
|
||||
const detailsContent = document.createElement("div");
|
||||
detailsContent.className = "a07-register__agreement-content";
|
||||
detailsContent.textContent = contentText;
|
||||
container.append(summary, content);
|
||||
attachCollapsible(container);
|
||||
|
||||
details.appendChild(detailsContent);
|
||||
|
||||
summary.addEventListener("click", (e) => {
|
||||
if (e.target !== input) {
|
||||
details.open = !details.open;
|
||||
}
|
||||
});
|
||||
|
||||
container.append(summary, details);
|
||||
|
||||
return { root: container, input };
|
||||
return { root: container, input: box.input };
|
||||
}
|
||||
|
||||
async function loadAgreementText(filename: string): Promise<string> {
|
||||
@@ -69,8 +63,6 @@ async function loadAgreementText(filename: string): Promise<string> {
|
||||
export function renderA07Register(root: HTMLElement): void {
|
||||
const page = document.createElement("div");
|
||||
page.className = "a07-register";
|
||||
const card = document.createElement("div");
|
||||
card.className = "a07-register__card";
|
||||
const title = document.createElement("h1");
|
||||
title.className = "a07-register__title";
|
||||
title.textContent = L("A07_Register_Title");
|
||||
@@ -210,7 +202,8 @@ export function renderA07Register(root: HTMLElement): void {
|
||||
variant: "ghost",
|
||||
onClick: () => navigateTo(ROUTES.A06_LOGIN),
|
||||
});
|
||||
card.append(title, form, login);
|
||||
const card = createCard({ body: [title, form, login] });
|
||||
card.classList.add("a07-register__card");
|
||||
page.append(card);
|
||||
root.append(page);
|
||||
});
|
||||
|
||||
@@ -1,141 +0,0 @@
|
||||
/* =============================================================================
|
||||
* A07_Register_UI_Page.ts
|
||||
* 로그인 전 07: 회원가입 (회사명 + 이름 + 이메일 + 비밀번호 폼)
|
||||
*
|
||||
* 제약 준수:
|
||||
* - 모든 문구는 ui_template_locale.ts 참조 (하드코딩 금지).
|
||||
* - 색상/간격은 A07_Register_UI_Style.css + theme.css 변수만 사용.
|
||||
* - 공통 컴포넌트(createButton/createInputField) 우선 사용.
|
||||
* - 이벤트 핸들러는 on[페이지]_[기능]_[액션] 명명.
|
||||
* - 백엔드 전송 전 1차 유효성 검사 (frontend.md §4).
|
||||
* ========================================================================== */
|
||||
|
||||
import { ui_locales, currentLanguageIndex } from "@ui/ui_template_locale";
|
||||
import { createButton, createInputField } from "@ui/ui_template_elements";
|
||||
import { isBlank, isValidEmail, hasMinLength } from "@util/common_util_validate";
|
||||
import { navigateTo } from "../A00_Common/router";
|
||||
import { ROUTES } from "@config/config_frontend";
|
||||
import "./A07_Register_UI_Style.css";
|
||||
|
||||
/** locale 헬퍼 */
|
||||
function L(key: keyof typeof ui_locales): string {
|
||||
return ui_locales[key][currentLanguageIndex];
|
||||
}
|
||||
|
||||
/** 비밀번호 최소 길이 */
|
||||
const PASSWORD_MIN_LENGTH = 8;
|
||||
|
||||
/* -----------------------------------------------------------------------------
|
||||
* 페이지 진입점
|
||||
* -------------------------------------------------------------------------- */
|
||||
export function renderA07Register(root: HTMLElement): void {
|
||||
const page = document.createElement("div");
|
||||
page.className = "a07-register";
|
||||
|
||||
const card = document.createElement("div");
|
||||
card.className = "a07-register__card";
|
||||
|
||||
const title = document.createElement("h1");
|
||||
title.className = "a07-register__title";
|
||||
title.textContent = L("A07_Register_Title");
|
||||
|
||||
const subtitle = document.createElement("p");
|
||||
subtitle.className = "a07-register__subtitle";
|
||||
subtitle.textContent = L("A07_Register_Subtitle");
|
||||
|
||||
// 입력 필드
|
||||
const companyField = createInputField({
|
||||
label: L("A07_Register_Field_Company"),
|
||||
placeholder: L("A07_Register_Field_Company_Placeholder"),
|
||||
type: "text",
|
||||
required: true,
|
||||
});
|
||||
const nameField = createInputField({
|
||||
label: L("A07_Register_Field_Name"),
|
||||
placeholder: L("A07_Register_Field_Name_Placeholder"),
|
||||
type: "text",
|
||||
required: true,
|
||||
});
|
||||
const emailField = createInputField({
|
||||
label: L("A07_Register_Field_Email"),
|
||||
placeholder: L("A07_Register_Field_Email_Placeholder"),
|
||||
type: "email",
|
||||
required: true,
|
||||
});
|
||||
const passwordField = createInputField({
|
||||
label: L("A07_Register_Field_Password"),
|
||||
placeholder: L("A07_Register_Field_Password_Placeholder"),
|
||||
type: "password",
|
||||
required: true,
|
||||
});
|
||||
|
||||
const form = document.createElement("form");
|
||||
form.className = "a07-register__form";
|
||||
form.noValidate = true;
|
||||
|
||||
const submitBtn = createButton({
|
||||
label: L("A07_Register_Submit"),
|
||||
variant: "filled",
|
||||
type: "submit",
|
||||
});
|
||||
submitBtn.classList.add("a07-register__submit");
|
||||
|
||||
/* 이벤트 핸들러: 제출 시 1차 유효성 검사 */
|
||||
function onA07_Register_Submit_Click(event: Event): void {
|
||||
event.preventDefault();
|
||||
[companyField, nameField, emailField, passwordField].forEach((f) => f.setError());
|
||||
|
||||
const company = companyField.input.value;
|
||||
const name = nameField.input.value;
|
||||
const email = emailField.input.value;
|
||||
const password = passwordField.input.value;
|
||||
|
||||
// 필수값 검사
|
||||
let hasError = false;
|
||||
if (isBlank(company)) {
|
||||
companyField.setError(L("A07_Register_Error_Required"));
|
||||
hasError = true;
|
||||
}
|
||||
if (isBlank(name)) {
|
||||
nameField.setError(L("A07_Register_Error_Required"));
|
||||
hasError = true;
|
||||
}
|
||||
if (isBlank(email)) {
|
||||
emailField.setError(L("A07_Register_Error_Required"));
|
||||
hasError = true;
|
||||
}
|
||||
if (isBlank(password)) {
|
||||
passwordField.setError(L("A07_Register_Error_Required"));
|
||||
hasError = true;
|
||||
}
|
||||
if (hasError) return;
|
||||
|
||||
// 형식 검사
|
||||
if (!isValidEmail(email)) {
|
||||
emailField.setError(L("A07_Register_Error_Email"));
|
||||
return;
|
||||
}
|
||||
if (!hasMinLength(password, PASSWORD_MIN_LENGTH)) {
|
||||
passwordField.setError(L("A07_Register_Error_Password"));
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: POST /api/a07/register 연동. 성공 시 로그인 페이지로.
|
||||
navigateTo(ROUTES.A06_LOGIN);
|
||||
}
|
||||
|
||||
form.addEventListener("submit", onA07_Register_Submit_Click);
|
||||
form.append(companyField.root, nameField.root, emailField.root, passwordField.root, submitBtn);
|
||||
|
||||
// 로그인 링크
|
||||
const toLogin = createButton({
|
||||
label: L("A07_Register_ToLogin"),
|
||||
variant: "ghost",
|
||||
onClick: () => navigateTo(ROUTES.A06_LOGIN),
|
||||
});
|
||||
toLogin.classList.add("a07-register__link");
|
||||
|
||||
card.append(title, subtitle, form, toLogin);
|
||||
page.append(card);
|
||||
root.append(page);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/* =============================================================================
|
||||
* A07_Register_UI_Style.css
|
||||
* 회원가입 페이지 고유 스타일 — theme.css 변수만 참조 (하드코딩 금지).
|
||||
* 회원가입 페이지 고유 배치 — 카드 · 체크 · 접이 겉은 공용 부품이 냄.
|
||||
* ========================================================================== */
|
||||
|
||||
.a07-register {
|
||||
@@ -13,49 +13,22 @@
|
||||
.a07-register__card {
|
||||
width: 100%;
|
||||
max-width: 420px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-16);
|
||||
padding: var(--spacing-40);
|
||||
background-color: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-cards);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.a07-register__title {
|
||||
font-size: var(--text-heading-sm);
|
||||
line-height: var(--leading-heading-sm);
|
||||
color: var(--color-deep-iris);
|
||||
color: var(--color-text);
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
}
|
||||
.a07-register__subtitle {
|
||||
font-size: var(--text-body-sm);
|
||||
color: var(--color-slate);
|
||||
text-align: center;
|
||||
margin: 0 0 var(--spacing-8);
|
||||
}
|
||||
|
||||
.a07-register__form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-16);
|
||||
}
|
||||
.a07-register__submit {
|
||||
width: 100%;
|
||||
margin-top: var(--spacing-8);
|
||||
}
|
||||
.a07-register__link {
|
||||
width: 100%;
|
||||
}
|
||||
.a07-register__check {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-8);
|
||||
color: var(--color-text);
|
||||
font-size: var(--text-body-sm);
|
||||
}
|
||||
|
||||
.a07-register__agreement {
|
||||
border: 1px solid var(--color-border);
|
||||
@@ -64,17 +37,9 @@
|
||||
}
|
||||
|
||||
.a07-register__agreement-summary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-8);
|
||||
padding: var(--spacing-12) var(--spacing-16);
|
||||
background-color: var(--color-canvas);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.a07-register__agreement-summary input[type="checkbox"] {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.a07-register__agreement-label {
|
||||
@@ -83,20 +48,14 @@
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.a07-register__agreement-details {
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.a07-register__agreement-details[open] {
|
||||
padding: var(--spacing-12) var(--spacing-16);
|
||||
}
|
||||
|
||||
.a07-register__agreement-content {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
padding: var(--spacing-12) var(--spacing-16);
|
||||
border-top: 1px solid var(--color-border);
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
font-size: var(--text-body-sm);
|
||||
color: var(--color-slate);
|
||||
color: var(--color-text-secondary);
|
||||
line-height: var(--leading-relaxed);
|
||||
}
|
||||
|
||||
@@ -5,13 +5,14 @@
|
||||
* 제약 준수:
|
||||
* - 모든 문구는 ui_template_locale.ts 참조 (하드코딩 금지).
|
||||
* - 색상/간격은 A08_Support_UI_Style.css + theme.css 변수만 사용.
|
||||
* - 공통 컴포넌트(createButton/createInputField) 우선 사용.
|
||||
* - 공통 부품(createCard/createBanner/createInputField/createButton) 우선 사용.
|
||||
* - 이벤트 핸들러는 on[페이지]_[기능]_[액션] 명명.
|
||||
* - 백엔드 전송 전 1차 유효성 검사 (frontend.md §4).
|
||||
* ========================================================================== */
|
||||
|
||||
import { ui_locales, currentLanguageIndex } from "@ui/ui_template_locale";
|
||||
import { createButton, createInputField } from "@ui/ui_template_elements";
|
||||
import { createBanner } from "@ui/ui_template_banner";
|
||||
import { createButton, createCard, createInputField } from "@ui/ui_template_elements";
|
||||
import { isBlank, isValidEmail } from "@util/common_util_validate";
|
||||
import { fetchSessionUser } from "../A06_Login/A06_Login_Api_Fetch";
|
||||
import "./A08_Support_UI_Style.css";
|
||||
@@ -21,48 +22,6 @@ function L(key: keyof typeof ui_locales): string {
|
||||
return ui_locales[key][currentLanguageIndex];
|
||||
}
|
||||
|
||||
/** 문의내용 textarea 핸들 (createInputField와 유사한 형태로 에러 슬롯 포함) */
|
||||
interface TextAreaHandle {
|
||||
root: HTMLDivElement;
|
||||
textarea: HTMLTextAreaElement;
|
||||
setError: (msg?: string) => void;
|
||||
}
|
||||
|
||||
/** 여러 줄 입력용 textarea 필드 생성 (공통 .ui-field/.ui-input 스타일 상속). */
|
||||
function createTextAreaField(label: string, placeholder: string): TextAreaHandle {
|
||||
const root = document.createElement("div");
|
||||
root.className = "ui-field";
|
||||
|
||||
const labelEl = document.createElement("label");
|
||||
labelEl.className = "ui-field__label";
|
||||
labelEl.textContent = label;
|
||||
|
||||
const textarea = document.createElement("textarea");
|
||||
textarea.className = "ui-input a08-support__textarea";
|
||||
textarea.placeholder = placeholder;
|
||||
textarea.rows = 6;
|
||||
|
||||
const errorSlot = document.createElement("span");
|
||||
errorSlot.className = "ui-field__error";
|
||||
errorSlot.setAttribute("role", "alert");
|
||||
|
||||
root.append(labelEl, textarea, errorSlot);
|
||||
|
||||
const setError = (msg?: string): void => {
|
||||
if (msg) {
|
||||
textarea.classList.add("ui-input--error");
|
||||
errorSlot.textContent = msg;
|
||||
errorSlot.classList.add("is-visible");
|
||||
} else {
|
||||
textarea.classList.remove("ui-input--error");
|
||||
errorSlot.textContent = "";
|
||||
errorSlot.classList.remove("is-visible");
|
||||
}
|
||||
};
|
||||
|
||||
return { root, textarea, setError };
|
||||
}
|
||||
|
||||
/* -----------------------------------------------------------------------------
|
||||
* 페이지 진입점
|
||||
* -------------------------------------------------------------------------- */
|
||||
@@ -70,9 +29,6 @@ export function renderA08Support(root: HTMLElement): void {
|
||||
const page = document.createElement("div");
|
||||
page.className = "a08-support";
|
||||
|
||||
const card = document.createElement("div");
|
||||
card.className = "a08-support__card";
|
||||
|
||||
const title = document.createElement("h1");
|
||||
title.className = "a08-support__title";
|
||||
title.textContent = L("A08_Support_Title");
|
||||
@@ -82,9 +38,7 @@ export function renderA08Support(root: HTMLElement): void {
|
||||
subtitle.textContent = L("A08_Support_Subtitle");
|
||||
|
||||
// 성공 메시지 슬롯 (제출 성공 시 표시)
|
||||
const successMsg = document.createElement("p");
|
||||
successMsg.className = "a08-support__success";
|
||||
successMsg.textContent = L("A08_Support_Success");
|
||||
const successMsg = createBanner({ tone: "success", text: L("A08_Support_Success") });
|
||||
successMsg.hidden = true;
|
||||
|
||||
// 입력 필드
|
||||
@@ -113,10 +67,11 @@ export function renderA08Support(root: HTMLElement): void {
|
||||
type: "text",
|
||||
required: true,
|
||||
});
|
||||
const messageField = createTextAreaField(
|
||||
L("A08_Support_Field_Message"),
|
||||
L("A08_Support_Field_Message_Placeholder"),
|
||||
);
|
||||
const messageField = createInputField({
|
||||
label: L("A08_Support_Field_Message"),
|
||||
placeholder: L("A08_Support_Field_Message_Placeholder"),
|
||||
multiline: 6,
|
||||
});
|
||||
|
||||
const form = document.createElement("form");
|
||||
form.className = "a08-support__form";
|
||||
@@ -142,7 +97,7 @@ export function renderA08Support(root: HTMLElement): void {
|
||||
const email = emailField.input.value;
|
||||
const phone = phoneField.input.value;
|
||||
const subject = subjectField.input.value;
|
||||
const message = messageField.textarea.value;
|
||||
const message = messageField.input.value;
|
||||
|
||||
// 필수값 검사
|
||||
let hasError = false;
|
||||
@@ -184,7 +139,7 @@ export function renderA08Support(root: HTMLElement): void {
|
||||
throw new Error(error.detail ?? "요청 실패");
|
||||
}
|
||||
form.reset();
|
||||
messageField.textarea.value = "";
|
||||
messageField.input.value = "";
|
||||
successMsg.hidden = false;
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : "오류 발생";
|
||||
@@ -203,7 +158,8 @@ export function renderA08Support(root: HTMLElement): void {
|
||||
submitBtn,
|
||||
);
|
||||
|
||||
card.append(title, subtitle, successMsg, form);
|
||||
const card = createCard({ body: [title, subtitle, successMsg, form] });
|
||||
card.classList.add("a08-support__card");
|
||||
page.append(card);
|
||||
root.append(page);
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* =============================================================================
|
||||
* A08_Support_UI_Style.css
|
||||
* 기술지원 요청 페이지 고유 스타일 — theme.css 변수만 참조 (하드코딩 금지).
|
||||
* 기술지원 요청 페이지 고유 배치 — 카드 · 알림 · 칸 겉은 공용 부품이 냄.
|
||||
* ========================================================================== */
|
||||
|
||||
.a08-support {
|
||||
@@ -13,50 +13,28 @@
|
||||
.a08-support__card {
|
||||
width: 100%;
|
||||
max-width: 520px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-16);
|
||||
padding: var(--spacing-40);
|
||||
background-color: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-cards);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.a08-support__title {
|
||||
font-size: var(--text-heading-sm);
|
||||
line-height: var(--leading-heading-sm);
|
||||
color: var(--color-deep-iris);
|
||||
color: var(--color-text);
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
}
|
||||
.a08-support__subtitle {
|
||||
font-size: var(--text-body-sm);
|
||||
color: var(--color-slate);
|
||||
color: var(--color-text-secondary);
|
||||
text-align: center;
|
||||
margin: 0 0 var(--spacing-8);
|
||||
}
|
||||
|
||||
.a08-support__success {
|
||||
margin: 0;
|
||||
padding: var(--spacing-16);
|
||||
text-align: center;
|
||||
color: var(--color-success);
|
||||
background-color: var(--color-mist-violet);
|
||||
border-radius: var(--radius-cards);
|
||||
font-size: var(--text-body-sm);
|
||||
}
|
||||
|
||||
.a08-support__form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-16);
|
||||
}
|
||||
.a08-support__textarea {
|
||||
resize: vertical;
|
||||
min-height: 120px;
|
||||
font-family: inherit;
|
||||
}
|
||||
.a08-support__submit {
|
||||
width: 100%;
|
||||
margin-top: var(--spacing-8);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { createCard } from "@ui/ui_template_elements";
|
||||
import { currentLanguageIndex, ui_locales } from "@ui/ui_template_locale";
|
||||
import { SECURITY_TERMS_VERSION, securityTerms } from "./A09_Security_Terms";
|
||||
import "./A09_Security_UI_Style.css";
|
||||
@@ -5,13 +6,11 @@ import "./A09_Security_UI_Style.css";
|
||||
const L = (key: keyof typeof ui_locales): string => ui_locales[key][currentLanguageIndex];
|
||||
|
||||
export function renderA09Security(root: HTMLElement): void {
|
||||
const page = document.createElement("article");
|
||||
page.className = "a09-security";
|
||||
const title = document.createElement("h1");
|
||||
title.textContent = L("A09_Security_Title");
|
||||
const version = document.createElement("p");
|
||||
version.textContent = `${L("A09_Security_Version")} ${SECURITY_TERMS_VERSION}`;
|
||||
page.append(title, version);
|
||||
const blocks: HTMLElement[] = [title, version];
|
||||
const sections = [
|
||||
["A09_Security_Terms", securityTerms.terms],
|
||||
["A09_Security_Privacy", securityTerms.privacy],
|
||||
@@ -24,7 +23,9 @@ export function renderA09Security(root: HTMLElement): void {
|
||||
const paragraph = document.createElement("p");
|
||||
paragraph.textContent = body[currentLanguageIndex];
|
||||
section.append(heading, paragraph);
|
||||
page.append(section);
|
||||
blocks.push(section);
|
||||
}
|
||||
const page = createCard({ body: blocks });
|
||||
page.classList.add("a09-security");
|
||||
root.append(page);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
/* 약관 문서 — 카드 겉은 공용 부품(카드) · 여기는 폭 · 여백 · 글 흐름만 */
|
||||
.a09-security {
|
||||
max-width: 800px;
|
||||
margin: var(--spacing-64) auto;
|
||||
padding: var(--spacing-40);
|
||||
color: var(--color-text);
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-cards);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.a09-security > .ui-card__body {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.a09-security section {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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<void> {
|
||||
// 대시보드 임시 보관함에서 가져올 자료 선택기 — 선택되면 [업로드]가 이동을 수행한다.
|
||||
const tempPicker = createTempPicker(() => updateUploadButton());
|
||||
const cardMap = new Map<FileSlot, HTMLElement>();
|
||||
const progressMap = new Map<FileSlot, ProgressBarHandle>();
|
||||
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<void> {
|
||||
? 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<void> {
|
||||
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<void> {
|
||||
|
||||
const fileName = card.querySelector<HTMLSpanElement>(".b03-file__file-name");
|
||||
const fileSize = card.querySelector<HTMLSpanElement>(".b03-file__file-size");
|
||||
const progress = card.querySelector<HTMLDivElement>(".b03-file__progress-bar");
|
||||
const progress = progressMap.get(slot);
|
||||
const progressBytes = card.querySelector<HTMLSpanElement>(".b03-file__progress-bytes");
|
||||
const progressSpeed = card.querySelector<HTMLSpanElement>(".b03-file__progress-speed");
|
||||
const progressEta = card.querySelector<HTMLSpanElement>(".b03-file__progress-eta");
|
||||
const error = card.querySelector<HTMLDivElement>(".b03-file__error-message");
|
||||
const error = card.querySelector<HTMLElement>(".b03-file__error-message .ui-banner__text");
|
||||
const remove = card.querySelector<HTMLButtonElement>(".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<void> {
|
||||
? `${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<void> {
|
||||
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<void> {
|
||||
renderExtensionLabel(card, state);
|
||||
const input = card.querySelector<HTMLInputElement>(".b03-file__slot-input")!;
|
||||
input.accept = state.extensions.join(",");
|
||||
const select = card.querySelector<HTMLButtonElement>(".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<HTMLButtonElement>(".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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
);
|
||||
|
||||
// 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<void> {
|
||||
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<void> {
|
||||
// 골라 놓고도 [파일 업로드]가 잠긴 채 남는다(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");
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
}),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 보다 좁아질 수 없다**는 뜻이라, 컨테이너가
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -227,7 +227,7 @@ export function createFileCardTemplate(): HTMLTemplateElement {
|
||||
const template = document.createElement("template");
|
||||
template.id = "file-card-template";
|
||||
template.innerHTML = `
|
||||
<article class="b03-file__card b03-file__card--empty">
|
||||
<article class="ui-card b03-file__card b03-file__card--empty" data-pad="12">
|
||||
<div class="b03-file__card-header">
|
||||
<span class="b03-file__card-icon" aria-hidden="true"></span>
|
||||
<div class="b03-file__card-heading">
|
||||
@@ -235,8 +235,8 @@ export function createFileCardTemplate(): HTMLTemplateElement {
|
||||
<span class="b03-file__card-ext"></span>
|
||||
</div>
|
||||
<div class="b03-file__card-badge-container"></div>
|
||||
<button class="b03-file__card-select" type="button"></button>
|
||||
<button class="b03-file__card-remove" type="button"></button>
|
||||
<span data-slot="select"></span>
|
||||
<span data-slot="remove"></span>
|
||||
</div>
|
||||
<div class="b03-file__card-content">
|
||||
<input class="b03-file__slot-input" type="file" />
|
||||
@@ -246,16 +246,14 @@ export function createFileCardTemplate(): HTMLTemplateElement {
|
||||
</div>
|
||||
<div class="b03-file__preview"></div>
|
||||
<div class="b03-file__progress-section">
|
||||
<div class="b03-file__progress-bar-container">
|
||||
<div class="b03-file__progress-bar"></div>
|
||||
</div>
|
||||
<span data-slot="progress"></span>
|
||||
<div class="b03-file__progress-info">
|
||||
<span class="b03-file__progress-bytes"></span>
|
||||
<span class="b03-file__progress-speed"></span>
|
||||
<span class="b03-file__progress-eta"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="b03-file__error-message" role="alert"></div>
|
||||
<span data-slot="error"></span>
|
||||
</div>
|
||||
</article>
|
||||
`;
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { createButton, createTag } from "@ui/ui_template_elements";
|
||||
import { createBadge } from "@ui/ui_template_badge";
|
||||
import { createBanner } from "@ui/ui_template_banner";
|
||||
import { createButton } from "@ui/ui_template_elements";
|
||||
import { section, table, text } from "@ui/ui_template_general_blocks";
|
||||
import { createGeneralLayout } from "@ui/ui_template_general_layout";
|
||||
import { currentLanguageIndex, ui_locales } from "@ui/ui_template_locale";
|
||||
@@ -13,15 +15,13 @@ export function renderB10Payment(root: HTMLElement): void {
|
||||
invoiceBody.className = "b10-payment__summary";
|
||||
const invoiceDescription = document.createElement("p");
|
||||
invoiceDescription.textContent = L("B10_Payment_Invoice_Description");
|
||||
invoiceBody.append(invoiceDescription, createTag(L("B10_Payment_Invoice_Status"), "neutral"));
|
||||
invoiceBody.append(invoiceDescription, createBadge(L("B10_Payment_Invoice_Status"), "neutral"));
|
||||
|
||||
const invoiceSection = section(L("B10_Payment_Invoice_Title"), invoiceBody, true, [
|
||||
createButton({ label: L("B10_Payment_Invoice_Request"), disabled: true }),
|
||||
]);
|
||||
|
||||
const depositNote = document.createElement("p");
|
||||
depositNote.className = "b10-payment__note";
|
||||
depositNote.textContent = L("B10_Payment_Deposit_Note");
|
||||
const depositNote = createBanner({ tone: "info", text: L("B10_Payment_Deposit_Note") });
|
||||
const depositBody = document.createElement("div");
|
||||
depositBody.className = "b10-payment__summary";
|
||||
depositBody.append(
|
||||
|
||||
@@ -4,11 +4,3 @@
|
||||
gap: var(--spacing-16);
|
||||
color: var(--color-text-body);
|
||||
}
|
||||
|
||||
.b10-payment__note {
|
||||
padding: var(--spacing-16);
|
||||
border-radius: var(--radius-cards);
|
||||
background: var(--color-surface);
|
||||
color: var(--color-text-secondary);
|
||||
font-size: var(--text-body-sm);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { createButton, createTag } from "@ui/ui_template_elements";
|
||||
import { createBadge } from "@ui/ui_template_badge";
|
||||
import { createButton, createCard } from "@ui/ui_template_elements";
|
||||
import { section } from "@ui/ui_template_general_blocks";
|
||||
import { createGeneralLayout } from "@ui/ui_template_general_layout";
|
||||
import { currentLanguageIndex, ui_locales } from "@ui/ui_template_locale";
|
||||
@@ -9,8 +10,9 @@ function L(key: keyof typeof ui_locales): string {
|
||||
}
|
||||
|
||||
function buildStatusFlow(): HTMLElement {
|
||||
const flow = document.createElement("ol");
|
||||
const flow = document.createElement("div");
|
||||
flow.className = "b11-status__flow";
|
||||
flow.setAttribute("role", "list");
|
||||
const steps = [
|
||||
L("B11_Status_Step_Request"),
|
||||
L("B11_Status_Step_Issue"),
|
||||
@@ -18,11 +20,14 @@ function buildStatusFlow(): HTMLElement {
|
||||
L("B11_Status_Step_Complete"),
|
||||
];
|
||||
for (const label of steps) {
|
||||
const item = document.createElement("li");
|
||||
item.className = "b11-status__step";
|
||||
const title = document.createElement("strong");
|
||||
title.textContent = label;
|
||||
item.append(title, createTag(L("B11_Status_NotStarted"), "neutral"));
|
||||
const item = createCard({
|
||||
body: [title, createBadge(L("B11_Status_NotStarted"), "neutral")],
|
||||
pad: 16,
|
||||
});
|
||||
item.classList.add("b11-status__step");
|
||||
item.setAttribute("role", "listitem");
|
||||
flow.append(item);
|
||||
}
|
||||
return flow;
|
||||
|
||||
@@ -2,20 +2,10 @@
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: var(--spacing-16);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.b11-status__step {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.b11-status__step > .ui-card__body {
|
||||
gap: var(--spacing-12, var(--spacing-8));
|
||||
padding: var(--spacing-16);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-cards);
|
||||
background: var(--color-surface);
|
||||
color: var(--color-text-body);
|
||||
}
|
||||
|
||||
.b11-status__download {
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
} from "@ui/ui_template_elements";
|
||||
import { createGeneralLayout } from "@ui/ui_template_general_layout";
|
||||
import { createHelpButton } from "@ui/ui_template_help";
|
||||
import { createText } from "@ui/ui_template_text";
|
||||
import { t } from "@ui/ui_template_locale";
|
||||
import { stationLabel } from "../common_util/common_util_station";
|
||||
import { createGroupProject } from "../A00_Common/group_workflow";
|
||||
@@ -70,8 +71,7 @@ export function renderD01PlanRegister(root: HTMLElement): void {
|
||||
const endM = createInputField({ label: t("DE_Reg_EndM"), type: "number", min: 0 });
|
||||
// 누가거리 칸 옆 즉시 측점 표기(저장값 · 서버 검사는 m 그대로)
|
||||
for (const field of [startM, endM]) {
|
||||
const shown = document.createElement("span");
|
||||
shown.style.cssText = "font-size:12px;color:var(--color-text-muted)";
|
||||
const shown = createText("", "muted");
|
||||
shown.dataset.station = "";
|
||||
field.root.insertBefore(shown, field.root.lastElementChild);
|
||||
field.input.addEventListener("input", () => {
|
||||
|
||||
@@ -11,12 +11,14 @@ import "./D02_PlanBase_UI_Style.css";
|
||||
import { API_BASE_URL, CURRENT_PROJECT_ID_KEY, ROUTES } from "@config/config_frontend";
|
||||
import {
|
||||
createButton,
|
||||
createInputField,
|
||||
el,
|
||||
hideLoadingOverlay,
|
||||
showLoadingOverlay,
|
||||
showToast,
|
||||
} from "@ui/ui_template_elements";
|
||||
import { createPanelCard } from "@ui/ui_template_collapsible";
|
||||
import { createText } from "@ui/ui_template_text";
|
||||
import { editMap, type MapPin } from "@ui/ui_template_kakao_map";
|
||||
import { t } from "@ui/ui_template_locale";
|
||||
import { mountGroupStep } from "../A00_Common/group_workflow";
|
||||
@@ -66,9 +68,11 @@ export async function renderD02PlanBase(root: HTMLElement): Promise<void> {
|
||||
const projectId = localStorage.getItem(CURRENT_PROJECT_ID_KEY) ?? "";
|
||||
const mapHost = el("div", { className: "d02-map" });
|
||||
const table = el("div", { className: "d02-points" });
|
||||
const info = el("p", { className: "d02-info" });
|
||||
const info = createText("", "muted");
|
||||
info.classList.add("d02-info");
|
||||
const card = createPanelCard(t("D02_Points_Title"), false);
|
||||
const pointsHint = el("p", { className: "d02-info", text: t("D02_Points_Hint") });
|
||||
const pointsHint = createText(t("D02_Points_Hint"), "muted");
|
||||
pointsHint.classList.add("d02-info");
|
||||
card.append(pointsHint, table, info);
|
||||
const save = createButton({
|
||||
label: t("D02_Save"),
|
||||
@@ -139,12 +143,11 @@ export async function renderD02PlanBase(root: HTMLElement): Promise<void> {
|
||||
|
||||
function drawTable(): void {
|
||||
const rows = points.map((p, i) => {
|
||||
const lon = el("input", { className: "d02-points__input" }) as HTMLInputElement;
|
||||
const lat = el("input", { className: "d02-points__input" }) as HTMLInputElement;
|
||||
lon.type = lat.type = "number";
|
||||
const lon = createInputField({ type: "number", placeholder: t("D02_Lon"), size: 24 }).input;
|
||||
const lat = createInputField({ type: "number", placeholder: t("D02_Lat"), size: 24 }).input;
|
||||
lon.classList.add("d02-points__input");
|
||||
lat.classList.add("d02-points__input");
|
||||
lon.step = lat.step = "any";
|
||||
lon.placeholder = t("D02_Lon");
|
||||
lat.placeholder = t("D02_Lat");
|
||||
lon.value = p ? String(p.lon) : "";
|
||||
lat.value = p ? String(p.lat) : "";
|
||||
lon.disabled = lat.disabled = !editable;
|
||||
@@ -154,8 +157,11 @@ export async function renderD02PlanBase(root: HTMLElement): Promise<void> {
|
||||
const typed = (): void => {
|
||||
const at = { lon: Number.parseFloat(lon.value), lat: Number.parseFloat(lat.value) };
|
||||
const ok = at.lon >= 120 && at.lon <= 135 && at.lat >= 30 && at.lat <= 45;
|
||||
lon.classList.toggle("is-invalid", !ok);
|
||||
lat.classList.toggle("is-invalid", !ok);
|
||||
for (const box of [lon, lat]) {
|
||||
box.classList.toggle("is-error", !ok);
|
||||
if (ok) box.removeAttribute("aria-invalid");
|
||||
else box.setAttribute("aria-invalid", "true");
|
||||
}
|
||||
if (ok) place(i, at);
|
||||
};
|
||||
lon.addEventListener("change", typed);
|
||||
@@ -254,8 +260,6 @@ export async function renderD02PlanBase(root: HTMLElement): Promise<void> {
|
||||
active = firstEmpty();
|
||||
paint();
|
||||
} catch (error) {
|
||||
table.replaceChildren(
|
||||
el("p", { className: "d02-info", text: error instanceof Error ? error.message : "" }),
|
||||
);
|
||||
table.replaceChildren(createText(error instanceof Error ? error.message : "", "muted"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,17 +35,5 @@
|
||||
}
|
||||
|
||||
.d02-points__input {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
font-size: var(--text-caption);
|
||||
}
|
||||
|
||||
.d02-points__input.is-invalid {
|
||||
outline: 1px solid var(--color-danger, #d92d20);
|
||||
}
|
||||
|
||||
.d02-info {
|
||||
margin: 0;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: var(--text-caption);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
* ========================================================================== */
|
||||
|
||||
import "../B05_Profile/B05_Profile_UI_Style.css";
|
||||
import { createCheckbox } from "@ui/ui_template_checkbox";
|
||||
import { createSelectField } from "@ui/ui_template_input";
|
||||
import { openModal } from "@ui/ui_template_modal";
|
||||
import { createProgressCircle } from "@ui/ui_template_progress";
|
||||
import { showToast } from "@ui/ui_template_elements";
|
||||
@@ -51,17 +53,19 @@ function gridBounds(grid: DtmGrid): {
|
||||
export function openPlan3d(projectId: string, years: Plan3dYear[], firstYear: number): void {
|
||||
if (!years.length) return void showToast("3D 로 볼 연도 노선이 없습니다.", "warning");
|
||||
let viewer: RouteViewer | null = createRouteViewer();
|
||||
const pick = document.createElement("select");
|
||||
pick.className = "d03-plan__3d-year";
|
||||
for (const item of years)
|
||||
pick.append(new Option(`${item.year}년`, String(item.year), false, item.year === firstYear));
|
||||
const corridorBox = document.createElement("label");
|
||||
corridorBox.className = "d03-plan__3d-corridor";
|
||||
const corridorOn = Object.assign(document.createElement("input"), {
|
||||
type: "checkbox",
|
||||
const pick = createSelectField({
|
||||
options: years.map((item) => ({ value: String(item.year), text: `${item.year}년` })),
|
||||
value: String(firstYear),
|
||||
size: 28,
|
||||
ariaLabel: "연도",
|
||||
}).select;
|
||||
pick.classList.add("d03-plan__3d-year");
|
||||
const corridor = createCheckbox(" 예상 코리도(표준 횡단 기본값 · 계획고 = 지반고)", {
|
||||
checked: true,
|
||||
});
|
||||
corridorBox.append(corridorOn, " 예상 코리도(표준 횡단 기본값 · 계획고 = 지반고)");
|
||||
const corridorBox = corridor.root;
|
||||
corridorBox.classList.add("d03-plan__3d-corridor");
|
||||
const corridorOn = corridor.input;
|
||||
corridorOn.addEventListener("change", () => viewer?.setCorridorVisible(corridorOn.checked));
|
||||
const top = document.createElement("div");
|
||||
top.className = "d03-plan__3d-top";
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
import "../B05_Profile/B05_Profile_UI_Style_RouteEdit.css";
|
||||
import { CURRENT_PROJECT_ID_KEY } from "@config/config_frontend";
|
||||
import { createWorkflowPanelHandle } from "@ui/ui_template_overlay";
|
||||
import { createText } from "@ui/ui_template_text";
|
||||
import { stationLabel } from "../common_util/common_util_station";
|
||||
import type { DtmGrid } from "../common_util/common_util_surface_sampler";
|
||||
import { buildCrossSvg } from "../B05_Profile/B05_Profile_UI_RouteEdit_Cross_Draw";
|
||||
@@ -63,7 +64,7 @@ export function createPlanCross(): PlanCross {
|
||||
|
||||
const clear = (text: string): void => {
|
||||
head.textContent = "";
|
||||
body.replaceChildren(Object.assign(document.createElement("p"), { textContent: text }));
|
||||
body.replaceChildren(createText(text, "muted", 14));
|
||||
delete root.dataset.chainage;
|
||||
};
|
||||
clear("평면에서 측점을 누르면 그 자리 지반선 · 예상 횡단이 보입니다.");
|
||||
|
||||
@@ -15,7 +15,10 @@
|
||||
|
||||
import "./D03_PlanAnalysis_UI_Style.css";
|
||||
import { CURRENT_PROJECT_ID_KEY, ROUTES } from "@config/config_frontend";
|
||||
import { createCard } from "@ui/ui_template_card";
|
||||
import { attachCollapsible, createPanelCard } from "@ui/ui_template_collapsible";
|
||||
import { createProgressBar } from "@ui/ui_template_progress";
|
||||
import { createText } from "@ui/ui_template_text";
|
||||
import {
|
||||
createButton,
|
||||
hideLoadingOverlay,
|
||||
@@ -118,12 +121,10 @@ export async function renderD03PlanAnalysis(root: HTMLElement): Promise<void> {
|
||||
main.className = "d03-plan";
|
||||
main.append(map.root, cross.root);
|
||||
|
||||
const terrainText = document.createElement("p");
|
||||
terrainText.className = "d03-plan__hint";
|
||||
const bar = document.createElement("progress");
|
||||
bar.className = "d03-plan__bar";
|
||||
bar.max = 100;
|
||||
bar.hidden = true;
|
||||
const terrainText = createText("", "muted");
|
||||
terrainText.classList.add("d03-plan__hint");
|
||||
const bar = createProgressBar();
|
||||
bar.root.hidden = true;
|
||||
const more = createButton({
|
||||
label: "범위 밖 — 더 받기",
|
||||
variant: "filled",
|
||||
@@ -145,7 +146,7 @@ export async function renderD03PlanAnalysis(root: HTMLElement): Promise<void> {
|
||||
actions.className = "ui-sidebar-actions";
|
||||
actions.append(analyzeButton, saveButton, viewButton, undoButton);
|
||||
const terrainCard = createPanelCard("지형", false);
|
||||
terrainCard.append(terrainText, bar, more);
|
||||
terrainCard.append(terrainText, bar.root, more);
|
||||
const routeCard = createPanelCard("연도별 노선", false);
|
||||
routeCard.append(yearList);
|
||||
const panel = document.createElement("div");
|
||||
@@ -334,29 +335,33 @@ export async function renderD03PlanAnalysis(root: HTMLElement): Promise<void> {
|
||||
for (const mode of PLAN_MODES) {
|
||||
const route = result.routes[mode];
|
||||
if (!route) continue;
|
||||
const card = document.createElement("button");
|
||||
card.type = "button";
|
||||
card.className = "d03-plan__mode" + (row.mode === mode ? " is-picked" : "");
|
||||
card.dataset.mode = mode;
|
||||
const metrics = route.metrics;
|
||||
const values = createText(
|
||||
!metrics
|
||||
? "노선 없음"
|
||||
: metrics.max_grade_pct === undefined
|
||||
? `연장 ${metrics.length_m.toFixed(1)} m · 기울기 · 위반 = 저장 때 셈`
|
||||
: `연장 ${metrics.length_m.toFixed(1)} m · 최대 ${metrics.max_grade_pct.toFixed(1)} % · 위반 ${metrics.over_limit?.total ?? 0}`,
|
||||
"muted",
|
||||
);
|
||||
const card = createCard({
|
||||
title: MODE_LABEL[mode],
|
||||
body: [values],
|
||||
pad: 12,
|
||||
selected: row.mode === mode,
|
||||
onSelect: () => {
|
||||
if (row.mode === mode) return;
|
||||
remember();
|
||||
choices = { ...choices, [result.year]: mode };
|
||||
keepDraft();
|
||||
refresh();
|
||||
},
|
||||
});
|
||||
card.classList.add("d03-plan__mode");
|
||||
card.classList.toggle("is-picked", row.mode === mode);
|
||||
card.dataset.mode = mode;
|
||||
card.disabled = !!route.error || !metrics;
|
||||
card.title = route.error ?? "";
|
||||
const name = document.createElement("strong");
|
||||
name.textContent = MODE_LABEL[mode];
|
||||
const values = document.createElement("span");
|
||||
values.textContent = !metrics
|
||||
? "노선 없음"
|
||||
: metrics.max_grade_pct === undefined
|
||||
? `연장 ${metrics.length_m.toFixed(1)} m · 기울기 · 위반 = 저장 때 셈`
|
||||
: `연장 ${metrics.length_m.toFixed(1)} m · 최대 ${metrics.max_grade_pct.toFixed(1)} % · 위반 ${metrics.over_limit?.total ?? 0}`;
|
||||
card.append(name, values);
|
||||
card.addEventListener("click", () => {
|
||||
if (row.mode === mode) return;
|
||||
remember();
|
||||
choices = { ...choices, [result.year]: mode };
|
||||
keepDraft();
|
||||
refresh();
|
||||
});
|
||||
box.append(card);
|
||||
}
|
||||
return box;
|
||||
@@ -526,13 +531,13 @@ export async function renderD03PlanAnalysis(root: HTMLElement): Promise<void> {
|
||||
/** 뒤에서 도는 일을 끝날 때까지 지켜봄 — 진행은 지형 칸에. */
|
||||
async function watch(kind: "terrain" | "analyze"): Promise<PlanJob | null> {
|
||||
busy = kind;
|
||||
bar.hidden = false;
|
||||
bar.root.hidden = false;
|
||||
refresh();
|
||||
try {
|
||||
for (;;) {
|
||||
if (!root.isConnected) return null;
|
||||
const job = (await fetchPlanProgress(projectId))[kind];
|
||||
bar.value = job.percent;
|
||||
bar.set(job.percent / 100);
|
||||
terrainText.textContent = `${kind === "terrain" ? "지형 받기" : "분석"} ${job.percent}% · ${job.message ?? ""}`;
|
||||
if (!job.running) {
|
||||
if (job.error) showToast(job.error, "error");
|
||||
@@ -546,7 +551,7 @@ export async function renderD03PlanAnalysis(root: HTMLElement): Promise<void> {
|
||||
return null;
|
||||
} finally {
|
||||
busy = null;
|
||||
bar.hidden = true;
|
||||
bar.root.hidden = true;
|
||||
refresh();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,17 +63,10 @@
|
||||
|
||||
.d03-plan__cross-body p {
|
||||
margin: 12px 0;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.d03-plan__hint {
|
||||
margin: 0 0 6px;
|
||||
font-size: 0.85rem;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.d03-plan__bar {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.d03-plan__years {
|
||||
@@ -93,37 +86,10 @@
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.d03-plan__mode {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 2px;
|
||||
padding: 6px 8px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--color-surface);
|
||||
color: var(--color-text);
|
||||
font-size: 0.8rem;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.d03-plan__mode.is-picked {
|
||||
border-color: var(--color-primary);
|
||||
box-shadow: inset 0 0 0 1px var(--color-primary);
|
||||
}
|
||||
|
||||
.d03-plan__mode:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.d03-plan__mode span {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.d03-plan__3d-top {
|
||||
display: flex;
|
||||
gap: var(--spacing-8);
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import { API_BASE_URL, CURRENT_PROJECT_ID_KEY, ROUTES } from "@config/config_fro
|
||||
import { kstText } from "../common_util/common_util_time_kst";
|
||||
import {
|
||||
createButton,
|
||||
createInputField,
|
||||
createTag,
|
||||
el,
|
||||
hideLoadingOverlay,
|
||||
@@ -21,6 +22,8 @@ import {
|
||||
showLoadingOverlay,
|
||||
showToast,
|
||||
} from "@ui/ui_template_elements";
|
||||
import { createBanner } from "@ui/ui_template_banner";
|
||||
import { createCheckbox } from "@ui/ui_template_checkbox";
|
||||
import { createPanelCard } from "@ui/ui_template_collapsible";
|
||||
import { t } from "@ui/ui_template_locale";
|
||||
import { stationLabel } from "../common_util/common_util_station";
|
||||
@@ -146,14 +149,11 @@ export async function renderD04PlanConfirm(root: HTMLElement): Promise<void> {
|
||||
const listId = `d04-companies-${year}`;
|
||||
const options = el("datalist", { attrs: { id: listId } });
|
||||
const found = new Map<string, number>();
|
||||
const firm = el("input", {
|
||||
className: "d04-confirm__input",
|
||||
attrs: { list: listId, placeholder: t("D04_Company") },
|
||||
});
|
||||
const mail = el("input", {
|
||||
className: "d04-confirm__input",
|
||||
attrs: { type: "email", placeholder: t("D04_Email") },
|
||||
});
|
||||
const firm = createInputField({ placeholder: t("D04_Company"), size: 28 }).input;
|
||||
firm.setAttribute("list", listId);
|
||||
const mail = createInputField({ type: "email", placeholder: t("D04_Email"), size: 28 }).input;
|
||||
firm.classList.add("d04-confirm__input");
|
||||
mail.classList.add("d04-confirm__input");
|
||||
firm.addEventListener("input", () => {
|
||||
const query = firm.value.trim();
|
||||
if (!query) return;
|
||||
@@ -245,18 +245,20 @@ export async function renderD04PlanConfirm(root: HTMLElement): Promise<void> {
|
||||
className: "d04-confirm__table",
|
||||
children: [el("thead", { children: [head] }), el("tbody", { children: rows })],
|
||||
});
|
||||
const notes = draft()
|
||||
? [el("p", { className: "d04-confirm__dirty", text: t("D04_Dirty") })]
|
||||
: [];
|
||||
const notes = draft() ? [createBanner({ tone: "warning", text: t("D04_Dirty") })] : [];
|
||||
notes[0]?.classList.add("d04-confirm__dirty");
|
||||
main.replaceChildren(el("h2", { text: summary.name }), ...notes, table);
|
||||
|
||||
pickList.replaceChildren(
|
||||
...summary.years
|
||||
.filter((row) => row.saved)
|
||||
.map((row) => {
|
||||
const box = el("input", { attrs: { type: "checkbox", value: String(row.year) } });
|
||||
box.checked = !row.confirmed_at;
|
||||
return el("label", { className: "d04-confirm__pick", children: [box, ` ${row.year}`] });
|
||||
const pick = createCheckbox(String(row.year), {
|
||||
checked: !row.confirmed_at,
|
||||
value: String(row.year),
|
||||
});
|
||||
pick.root.classList.add("d04-confirm__pick");
|
||||
return pick.root;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -44,15 +44,6 @@
|
||||
|
||||
.d04-confirm__input {
|
||||
min-width: 10rem;
|
||||
padding: var(--spacing-4) var(--spacing-8);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--color-canvas);
|
||||
color: var(--color-text-body);
|
||||
}
|
||||
|
||||
.d04-confirm__dirty {
|
||||
color: var(--color-warning, var(--color-text-secondary));
|
||||
}
|
||||
|
||||
.d04-confirm__picks {
|
||||
|
||||
@@ -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 "<button" not in support # 날 단추 없음 — 단추는 부품이 만듦
|
||||
assert 'class="ui-card b03-file__card' in support # 파일 카드 = 카드 부품
|
||||
|
||||
|
||||
def test_b03_css_common_values_only():
|
||||
for name, css in _src("B03_FileInput", ".css").items():
|
||||
assert not OLD_ALIAS.search(css), f"{name}: 옛 보라 별칭"
|
||||
body = re.sub(r"/\*.*?\*/", "", css, flags=re.S)
|
||||
assert not RAW_COLOR.search(body), f"{name}: 날 색"
|
||||
|
||||
|
||||
def test_b02_no_dead_select_and_no_raw_widgets():
|
||||
css = "".join(_src("B02_ProjRegister", ".css").values())
|
||||
assert "b02-proj__select" not in css # 안 쓰던 고르기 화살표 CSS — 고르기 부품이 냄
|
||||
ts = "".join(_src("B02_ProjRegister", ".ts").values())
|
||||
assert "<button" not in ts and 'createElement("button")' not in ts
|
||||
@@ -0,0 +1,59 @@
|
||||
"""553-4 예정노선(D01~D04) · 안정성평가(E01~E07) 부품 옮김 지킴 시험 — 서버 · 화면 없이 소스만 본다.
|
||||
옮긴 페이지는 날 `<input>` · `<progress>` · 날 색 · 안 쓰는 변종 CSS 가 다시 생기면 안 됨."""
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
|
||||
|
||||
def _src(path: str) -> str:
|
||||
return (ROOT / path).read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def _no_hex(path: str) -> None:
|
||||
css = re.sub(r"/\*.*?\*/", "", _src(path), flags=re.S)
|
||||
assert not re.search(r"#[0-9a-fA-F]{3,8}\b", css), f"{path}: 날 색"
|
||||
|
||||
|
||||
def test_d01_station_label_is_text_part():
|
||||
ts = _src("D01_PlanRegister/D01_PlanRegister_UI_Page.ts")
|
||||
assert "style.cssText" not in ts
|
||||
assert 'createText("", "muted")' in ts
|
||||
assert 'dataset.station = ""' in ts # 시험 · 촬영이 잡음
|
||||
|
||||
|
||||
def test_d02_parts():
|
||||
ts = _src("D02_PlanBase/D02_PlanBase_UI_Page.ts")
|
||||
css = _src("D02_PlanBase/D02_PlanBase_UI_Style.css")
|
||||
assert 'el("input"' not in ts
|
||||
assert 'createInputField({ type: "number"' in ts
|
||||
assert 'classList.add("d02-info")' in ts # 촬영 장면이 `p.d02-info` 로 잡음
|
||||
for dead in (".d02-info", "is-invalid", "font-size: var(--text-caption);\n}\n\n.d02-info"):
|
||||
assert dead not in css
|
||||
assert "data-axis" in ts or "dataset.axis" in ts
|
||||
|
||||
|
||||
def test_d03_parts():
|
||||
ts = _src("D03_PlanAnalysis/D03_PlanAnalysis_UI_Page.ts")
|
||||
three = _src("D03_PlanAnalysis/D03_PlanAnalysis_UI_3D.ts")
|
||||
css = _src("D03_PlanAnalysis/D03_PlanAnalysis_UI_Style.css")
|
||||
assert 'createElement("progress")' not in ts and "createProgressBar()" in ts
|
||||
assert "createCard({" in ts and "onSelect:" in ts
|
||||
assert 'classList.toggle("is-picked"' in ts # 촬영 장면이 `.d03-plan__mode.is-picked` 로 잡음
|
||||
assert "card.dataset.mode = mode" in ts
|
||||
assert 'createElement("select")' not in three and "createSelectField" in three
|
||||
assert 'createElement("input")' not in three and "createCheckbox" in three
|
||||
for dead in (".d03-plan__bar", ".d03-plan__mode", "is-picked"):
|
||||
assert dead not in css, dead
|
||||
|
||||
|
||||
def test_d04_parts():
|
||||
ts = _src("D04_PlanConfirm/D04_PlanConfirm_UI_Page.ts")
|
||||
css = _src("D04_PlanConfirm/D04_PlanConfirm_UI_Style.css")
|
||||
assert 'el("input"' not in ts
|
||||
assert ts.count("createInputField(") == 2 and "createCheckbox(" in ts
|
||||
assert 'createBanner({ tone: "warning"' in ts
|
||||
assert "input:checked" in ts # [확정] 이 고른 연도를 읽는 길
|
||||
assert "border:" not in css.split(".d04-confirm__input")[1].split("}")[0]
|
||||
assert ".d04-confirm__dirty" not in css
|
||||
@@ -0,0 +1,117 @@
|
||||
"""553-7 공개 화면(A01~A09) · 로그인 · B10 · B11 부품 옮김 지킴 시험 — 서버 · 화면 없이 소스로.
|
||||
|
||||
· A01~A05 = 공개 화면 틀(createPublicFrame) 한 벌 — 페이지마다 hero · section 복제 CSS 없음
|
||||
· 카드 = createCard · 알림 = createBanner · 배지 = createBadge · 여러 줄 = createInputField(multiline)
|
||||
· 페이지 CSS 에 날 색(#hex · rgb · 옛 보라 이름) 없음 · 죽은 A07_Register_UI_Page 없음
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
|
||||
PUBLIC_PAGES = {
|
||||
"A01_Home/A01_Home_UI_Page.ts": ("A01_Home/A01_Home_UI_Style.css", "a01"),
|
||||
"A02_ProgDetail/A02_ProgDetail_UI_Page.ts": (
|
||||
"A02_ProgDetail/A02_ProgDetail_UI_Style.css",
|
||||
"a02",
|
||||
),
|
||||
"A03_CompDetail/A03_CompDetail_UI_Page.ts": (
|
||||
"A03_CompDetail/A03_CompDetail_UI_Style.css",
|
||||
"a03",
|
||||
),
|
||||
"A04_NewsHistory/A04_NewsHistory_UI_Page.ts": (
|
||||
"A04_NewsHistory/A04_NewsHistory_UI_Style.css",
|
||||
"a04",
|
||||
),
|
||||
"A05_EduDetail/A05_EduDetail_UI_Page.ts": ("A05_EduDetail/A05_EduDetail_UI_Style.css", "a05"),
|
||||
}
|
||||
CARD_PAGES = [
|
||||
"A06_Login/A06_Login_UI_Auth_Page.ts",
|
||||
"A07_Register/A07_Register_UI_Auth_Page.ts",
|
||||
"A08_Support/A08_Support_UI_Page.ts",
|
||||
"A09_Security/A09_Security_UI_Page.ts",
|
||||
]
|
||||
CSS_FILES = [css for css, _ in PUBLIC_PAGES.values()] + [
|
||||
"A06_Login/A06_Login_UI_Style.css",
|
||||
"A07_Register/A07_Register_UI_Style.css",
|
||||
"A08_Support/A08_Support_UI_Style.css",
|
||||
"A09_Security/A09_Security_UI_Style.css",
|
||||
"B10_Payment/B10_Payment_UI_Style.css",
|
||||
"B11_Status/B11_Status_UI_Style.css",
|
||||
]
|
||||
RAW_COLOR = re.compile(
|
||||
r"#[0-9a-fA-F]{3,8}\b|rgba?\(|hsla?\(|var\(--(?:b01|draw)-|"
|
||||
r"var\(--color-(?:deep-iris|mist-violet|slate|charcoal|ash|plum-velvet)\)"
|
||||
)
|
||||
|
||||
|
||||
def _text(rel: str) -> str:
|
||||
return (ROOT / rel).read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def _no_comments(css: str) -> str:
|
||||
return re.sub(r"/\*.*?\*/", "", css, flags=re.S)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("page", PUBLIC_PAGES)
|
||||
def test_public_page_uses_frame(page: str) -> None:
|
||||
src = _text(page)
|
||||
assert "createPublicFrame" in src
|
||||
for tag in ("hero", "__inner", "__subtitle", "__ctas"):
|
||||
assert tag not in src, f"{page}: 손 hero 조각 {tag} 남음"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("page", PUBLIC_PAGES)
|
||||
def test_public_css_has_no_hero_copy(page: str) -> None:
|
||||
css = _no_comments(_text(PUBLIC_PAGES[page][0]))
|
||||
prefix = PUBLIC_PAGES[page][1]
|
||||
assert f".{prefix}-hero" not in css
|
||||
assert f".{prefix}-section" not in css
|
||||
|
||||
|
||||
@pytest.mark.parametrize("page", CARD_PAGES)
|
||||
def test_card_pages_use_card_part(page: str) -> None:
|
||||
assert "createCard" in _text(page)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("css", CSS_FILES)
|
||||
def test_page_css_no_raw_color(css: str) -> None:
|
||||
assert not RAW_COLOR.search(_no_comments(_text(css)))
|
||||
|
||||
|
||||
def test_a07_agreement_uses_checkbox_and_collapsible() -> None:
|
||||
src = _text("A07_Register/A07_Register_UI_Auth_Page.ts")
|
||||
assert "createCheckbox" in src and "attachCollapsible" in src
|
||||
assert "<details" not in src and 'createElement("details")' not in src
|
||||
assert 'type = "checkbox"' not in src
|
||||
|
||||
|
||||
def test_a08_uses_banner_and_multiline_input() -> None:
|
||||
src = _text("A08_Support/A08_Support_UI_Page.ts")
|
||||
assert "createBanner" in src and "multiline" in src
|
||||
assert "createTextAreaField" not in src and 'createElement("textarea")' not in src
|
||||
|
||||
|
||||
def test_b10_b11_use_parts() -> None:
|
||||
b10 = _text("B10_Payment/B10_Payment_UI_Page.ts")
|
||||
b11 = _text("B11_Status/B11_Status_UI_Page.ts")
|
||||
assert "createBanner" in b10 and "createBadge" in b10 and "createTag" not in b10
|
||||
assert "createCard" in b11 and "createBadge" in b11 and "createTag" not in b11
|
||||
assert ".b10-payment__note" not in _text("B10_Payment/B10_Payment_UI_Style.css")
|
||||
assert ".b11-status__step {" not in _text("B11_Status/B11_Status_UI_Style.css")
|
||||
|
||||
|
||||
def test_dead_register_page_gone() -> None:
|
||||
assert not (ROOT / "A07_Register/A07_Register_UI_Page.ts").exists()
|
||||
|
||||
|
||||
def test_public_frame_wide_option() -> None:
|
||||
ts = _text("ui_template/ui_template_public_layout.ts")
|
||||
css = _text("ui_template/ui_template_public_layout.css")
|
||||
assert "wide?: boolean" in ts
|
||||
assert "[data-wide] .ui-public-frame__title" in css
|
||||
@@ -14,6 +14,9 @@
|
||||
font-size: 13px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
.ui-banner[hidden] {
|
||||
display: none;
|
||||
}
|
||||
.ui-banner--warning {
|
||||
--banner-tone: var(--color-warning);
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
.ui-public-frame__hero {
|
||||
padding: var(--spacing-96) var(--spacing-32);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
box-shadow: inset 0 -1px 0 var(--color-border);
|
||||
background: linear-gradient(to bottom, var(--color-accent-soft), var(--color-canvas));
|
||||
}
|
||||
|
||||
@@ -35,6 +35,14 @@
|
||||
line-height: var(--leading-heading-lg);
|
||||
}
|
||||
|
||||
.ui-public-frame__hero[data-wide] .ui-public-frame__title {
|
||||
max-width: 18ch;
|
||||
}
|
||||
|
||||
.ui-public-frame__hero[data-wide] .ui-public-frame__lead {
|
||||
max-width: 56ch;
|
||||
}
|
||||
|
||||
.ui-public-frame__lead {
|
||||
max-width: 52ch;
|
||||
margin: 0;
|
||||
@@ -66,6 +74,7 @@
|
||||
color: var(--color-text);
|
||||
font-size: var(--text-heading-sm);
|
||||
line-height: var(--leading-heading-sm);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.ui-public-frame__section-body {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* 공개 화면 틀 `public-frame`(552-4 틀) — 로그인 전 공개 페이지(A01 ~ A09) 몸통: 머리 띠(hero) · 구역 · 본문.
|
||||
* 꼴 = 머리 띠(제목 · 설명 · 주 단추 줄 · 가운데 / 왼쪽 맞춤) · 구역(제목 + 몸) 여럿
|
||||
* 공개 머리(로고 · 메뉴)와 바닥글(`.app-footer`)은 앱 셸 몫 — 이 틀은 그 사이 본문만
|
||||
* 크기 = 폭 = --page-max-width · 제목 = --text-heading-lg(좁으면 --text-heading) · 구역 제목 = --text-heading-sm
|
||||
* 크기 = 폭 = --page-max-width · 제목 = --text-heading-lg(좁으면 --text-heading · `wide` = 18 자) · 구역 제목 = --text-heading-sm(가운데)
|
||||
* 상태 = 없음
|
||||
* 쓰는 법 = const f = createPublicFrame({ title: "임도 설계", lead: "…", actions: [btn] }); f.addSection("기능").append(grid)
|
||||
* 대신하는 옛 변종 = A01 ~ A05 `.a0N-hero` · `__inner` · `__title` · `__subtitle` · `__ctas` 다섯 벌 · `.a0N-section(__title)`
|
||||
@@ -17,6 +17,8 @@ export interface PublicFrameOptions {
|
||||
actions?: HTMLElement[];
|
||||
/** 머리 띠 맞춤 — 기본 가운데 */
|
||||
align?: "center" | "start";
|
||||
/** 제목 18 자 · 설명 56 자 폭(기본 14 · 52) */
|
||||
wide?: boolean;
|
||||
}
|
||||
|
||||
export interface PublicFrame {
|
||||
@@ -41,6 +43,7 @@ export function createPublicFrame(opts: PublicFrameOptions): PublicFrame {
|
||||
const root = node("div", "ui-public-frame");
|
||||
const hero = node("section", "ui-public-frame__hero");
|
||||
hero.dataset.align = opts.align ?? "center";
|
||||
if (opts.wide) hero.dataset.wide = "";
|
||||
const inner = node("div", "ui-public-frame__hero-inner");
|
||||
inner.append(node("h1", "ui-public-frame__title", opts.title));
|
||||
if (opts.lead) inner.append(node("p", "ui-public-frame__lead", opts.lead));
|
||||
|
||||
Reference in New Issue
Block a user