260710_0
This commit is contained in:
@@ -141,6 +141,59 @@ export function createInputField(opts: InputFieldOptions): InputFieldHandle {
|
||||
return { root, input, setError };
|
||||
}
|
||||
|
||||
/* -----------------------------------------------------------------------------
|
||||
* 2-1. 드롭다운 필드 (Select Field) — design.md: input과 조화되는 8px radius, focus
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
||||
export interface SelectFieldOptions {
|
||||
/** 라벨 텍스트 */
|
||||
label?: string;
|
||||
/** 옵션 항목 목록 */
|
||||
options: { value: string; text: string }[];
|
||||
/** 초기 선택값 */
|
||||
value?: string;
|
||||
onChange?: (value: string) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export interface SelectFieldHandle {
|
||||
root: HTMLDivElement;
|
||||
select: HTMLSelectElement;
|
||||
}
|
||||
|
||||
export function createSelectField(opts: SelectFieldOptions): SelectFieldHandle {
|
||||
const root = el("div", { className: "ui-field" });
|
||||
|
||||
if (opts.label) {
|
||||
root.append(el("label", { className: "ui-field__label", text: opts.label }));
|
||||
}
|
||||
|
||||
const select = el("select", {
|
||||
className: "ui-select",
|
||||
});
|
||||
|
||||
for (const opt of opts.options) {
|
||||
const optEl = el("option", {
|
||||
attrs: { value: opt.value },
|
||||
text: opt.text,
|
||||
});
|
||||
if (opts.value !== undefined && opt.value === opts.value) {
|
||||
optEl.selected = true;
|
||||
}
|
||||
select.append(optEl);
|
||||
}
|
||||
|
||||
if (opts.disabled) select.disabled = true;
|
||||
|
||||
if (opts.onChange) {
|
||||
select.addEventListener("change", () => opts.onChange!(select.value));
|
||||
}
|
||||
|
||||
root.append(select);
|
||||
|
||||
return { root, select };
|
||||
}
|
||||
|
||||
/* -----------------------------------------------------------------------------
|
||||
* 3. 카드 / 패널 (Card) — design.md: white surface, 8px radius, shadow
|
||||
* -------------------------------------------------------------------------- */
|
||||
@@ -395,20 +448,31 @@ export function createLineChart(opts: LineChartOptions): HTMLDivElement {
|
||||
svg.append(tick);
|
||||
}
|
||||
|
||||
// x축 라벨 (데이터 포인트 개수만큼, X_TICK_STEP 간격으로 표기)
|
||||
// x축 라벨 (데이터 포인트 개수만큼, X_TICK_STEP 간격으로 표기) + 수직 점선 그리드
|
||||
if (opts.xLabels && opts.xLabels.length > 0) {
|
||||
const labels = opts.xLabels;
|
||||
const baseY = CHART_PAD.top + plotH;
|
||||
for (let idx = 0; idx < pointCount; idx += X_TICK_STEP) {
|
||||
const label = labels[idx];
|
||||
if (label === undefined || label === "") continue;
|
||||
const tick = document.createElementNS(svgNs, "text");
|
||||
tick.setAttribute("class", "ui-chart__tick ui-chart__tick--x");
|
||||
tick.setAttribute("x", String(xAt(idx)));
|
||||
tick.setAttribute("y", String(baseY + 16));
|
||||
tick.setAttribute("text-anchor", "middle");
|
||||
tick.textContent = label;
|
||||
svg.append(tick);
|
||||
const x = xAt(idx);
|
||||
if (label !== undefined && label !== "") {
|
||||
// 수직 점선 그리드
|
||||
const vline = document.createElementNS(svgNs, "line");
|
||||
vline.setAttribute("class", "ui-chart__grid ui-chart__grid--vertical");
|
||||
vline.setAttribute("x1", String(x));
|
||||
vline.setAttribute("x2", String(x));
|
||||
vline.setAttribute("y1", String(CHART_PAD.top));
|
||||
vline.setAttribute("y2", String(baseY));
|
||||
svg.append(vline);
|
||||
// x축 라벨
|
||||
const tick = document.createElementNS(svgNs, "text");
|
||||
tick.setAttribute("class", "ui-chart__tick ui-chart__tick--x");
|
||||
tick.setAttribute("x", String(x));
|
||||
tick.setAttribute("y", String(baseY + 16));
|
||||
tick.setAttribute("text-anchor", "middle");
|
||||
tick.textContent = label;
|
||||
svg.append(tick);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -432,17 +496,6 @@ export function createLineChart(opts: LineChartOptions): HTMLDivElement {
|
||||
path.setAttribute("class", `ui-chart__line ui-chart__line--c${s.colorIndex ?? 0}`);
|
||||
path.setAttribute("d", d.trim());
|
||||
svg.append(path);
|
||||
|
||||
// 각 데이터 포인트에 점(circle) 그리기
|
||||
s.values.forEach((v, i) => {
|
||||
if (v === null || v === undefined) return;
|
||||
const circle = document.createElementNS(svgNs, "circle");
|
||||
circle.setAttribute("class", `ui-chart__point ui-chart__point--c${s.colorIndex ?? 0}`);
|
||||
circle.setAttribute("cx", String(xAt(i)));
|
||||
circle.setAttribute("cy", String(yAt(v)));
|
||||
circle.setAttribute("r", "3");
|
||||
svg.append(circle);
|
||||
});
|
||||
}
|
||||
|
||||
// 범례 (그래프 영역 우상단 오버레이)
|
||||
@@ -477,12 +530,12 @@ const BASE_CSS = `
|
||||
justify-content: center;
|
||||
gap: var(--spacing-8);
|
||||
font-family: var(--font-body);
|
||||
font-size: var(--text-body);
|
||||
font-size: var(--text-body-sm);
|
||||
font-weight: var(--font-weight-medium);
|
||||
line-height: 1;
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--radius-buttons);
|
||||
padding: var(--spacing-16) var(--spacing-24);
|
||||
padding: var(--spacing-8) var(--spacing-16);
|
||||
cursor: pointer;
|
||||
transition: background-color var(--transition-fast),
|
||||
border-color var(--transition-fast), color var(--transition-fast);
|
||||
@@ -508,8 +561,6 @@ const BASE_CSS = `
|
||||
background-color: transparent;
|
||||
color: var(--color-plum-velvet);
|
||||
border-radius: var(--radius-pills);
|
||||
padding: var(--spacing-8) var(--spacing-16);
|
||||
font-size: var(--text-body-sm);
|
||||
}
|
||||
.ui-btn--pill:hover:not(:disabled),
|
||||
.ui-btn--pill.is-active { background-color: var(--color-mist-violet); }
|
||||
@@ -551,6 +602,34 @@ const BASE_CSS = `
|
||||
}
|
||||
.ui-field__error.is-visible { visibility: visible; }
|
||||
|
||||
.ui-select {
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
font-family: var(--font-body);
|
||||
font-size: var(--text-body-sm);
|
||||
color: var(--color-text-body);
|
||||
background-color: var(--color-canvas);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-inputs);
|
||||
padding: 10px 36px 10px 14px;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%233e0079'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M19 9l-7 7-7-7'/%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 12px center;
|
||||
background-size: 16px;
|
||||
cursor: pointer;
|
||||
transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
|
||||
}
|
||||
.ui-select:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-focus-ring);
|
||||
box-shadow: 0 0 0 1px var(--color-focus-ring);
|
||||
}
|
||||
.ui-select:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* --- Card --- */
|
||||
.ui-card {
|
||||
background-color: var(--color-surface-raised);
|
||||
@@ -697,6 +776,9 @@ const BASE_CSS = `
|
||||
stroke-width: 1;
|
||||
opacity: 0.5;
|
||||
}
|
||||
.ui-chart__grid--vertical {
|
||||
stroke-dasharray: 4, 4;
|
||||
}
|
||||
.ui-chart__tick {
|
||||
fill: var(--color-text-muted);
|
||||
font-size: 12px;
|
||||
|
||||
@@ -545,6 +545,45 @@ export const ui_locales = {
|
||||
B03_File_Upload_Success: ["입력 파일 업로드를 완료했습니다.", "Input files uploaded."],
|
||||
B03_File_Upload_Failed: ["파일 업로드에 실패했습니다.", "File upload failed."],
|
||||
B03_File_Result_Path: ["저장 경로", "Stored path"],
|
||||
B03_File_Group_Required: ["필수 파일", "Required files"],
|
||||
B03_File_Group_Optional: ["선택 파일", "Optional files"],
|
||||
B03_File_Slot_PointCloud: ["포인트클라우드", "Point Cloud"],
|
||||
B03_File_Slot_Projection: ["좌표계 정의", "Projection"],
|
||||
B03_File_Slot_RasterCoord: ["래스터 좌표", "Raster Coord"],
|
||||
B03_File_Slot_TerrainDem: ["지형 래스터", "Terrain DEM"],
|
||||
B03_File_Slot_CadDrawing: ["CAD 도면", "CAD Drawing"],
|
||||
B03_File_Card_Select: ["파일 선택", "Select file"],
|
||||
B03_File_Card_Remove: ["파일 제거", "Remove file"],
|
||||
B03_File_Error_DuplicateSlot: [
|
||||
"같은 유형의 파일이 이미 선택되어 있습니다.",
|
||||
"A file for this slot is already selected.",
|
||||
],
|
||||
B03_File_Error_RequiredSlots: [
|
||||
"필수 파일(LAS/LAZ, PRJ, TFW)을 모두 선택하세요.",
|
||||
"Select all required files: LAS/LAZ, PRJ, and TFW.",
|
||||
],
|
||||
B03_File_Error_SlotType: [
|
||||
"선택한 파일 유형이 이 카드와 맞지 않습니다.",
|
||||
"The selected file type does not match this card.",
|
||||
],
|
||||
B03_File_Progress_Bytes: ["진행", "Progress"],
|
||||
B03_File_Progress_Speed: ["속도", "Speed"],
|
||||
B03_File_Progress_Eta: ["예상 완료", "ETA"],
|
||||
B03_File_Status_Pending: ["대기", "Pending"],
|
||||
B03_File_Status_Uploading: ["업로드 중", "Uploading"],
|
||||
B03_File_Status_Completed: ["완료", "Completed"],
|
||||
B03_File_Status_Failed: ["실패", "Failed"],
|
||||
B03_File_Status_Detected: ["중단된 업로드 감지", "Paused upload detected"],
|
||||
B03_File_Resume_Button: ["업로드 재개", "Resume upload"],
|
||||
B03_File_New_Button: ["새 파일로 시작", "Start new file"],
|
||||
B03_File_ServiceWorker_Ready: [
|
||||
"백그라운드 업로드 준비가 완료되었습니다.",
|
||||
"Background upload is ready.",
|
||||
],
|
||||
B03_File_ServiceWorker_Unavailable: [
|
||||
"현재 브라우저에서는 백그라운드 업로드를 사용할 수 없습니다.",
|
||||
"Background upload is unavailable in this browser.",
|
||||
],
|
||||
|
||||
/* --- B04_wf1_Surface 지표면 모델 분석 --- */
|
||||
B04_Surface_Title: ["1차 · 지표면 모델 분석", "Step 1 · Surface Analysis"],
|
||||
@@ -697,8 +736,6 @@ export const ui_locales = {
|
||||
"시스템 관리자로 변경할 수 없습니다.",
|
||||
"Cannot change role to System Admin.",
|
||||
],
|
||||
B01_Dashboard_AutomationLogic: ["자동화 설계 로직", "Automation Logic"],
|
||||
B01_Dashboard_ChangeRole: ["역할 변경", "Change Role"],
|
||||
} as const satisfies Record<string, LocaleEntry>;
|
||||
|
||||
export type LocaleKey = keyof typeof ui_locales;
|
||||
|
||||
Reference in New Issue
Block a user