Files
Aislo/A07_Register/A07_Register_UI_Auth_Page.ts
T
eomsangdonandClaude Sonnet 5.5 25adc2f0e7 refactor(공개화면): A01~A09 · B10 · B11 을 공개 화면 틀 · 카드 · 알림 · 배지 · 체크 · 접이 부품으로 (PLAN 553-7)
- A01~A05 hero 다섯 벌 · 구역 제목 복제 CSS 걷고 공개 화면 틀 한 벌(wide 옵션 · 구역 제목 가운데 · 머리 아랫줄은 자리 안 먹는 그림자)
- 카드 = 카드 부품(로그인 · 가입 · 지원 · 약관 · 기능 카드 · 소식 줄 · B11 단계 카드) · 지원 성공 · B10 안내 = 띠 알림 · 태그 = 배지
- A07 약관 날 체크 · 날 details = 체크 + 접이 부품 · A08 날 textarea = 입력 여러 줄 · 빈 소식 = 빈 상태
- 죽은 A07_Register_UI_Page · 쓰이지 않는 CSS 걷음 · 띠 알림 hidden 받음
- 지킴 시험 test_553_7_public_pages

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

211 lines
7.5 KiB
TypeScript

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";
import "./A07_Register_UI_Style.css";
const L = (key: keyof typeof ui_locales): string => ui_locales[key][currentLanguageIndex];
const FOREST_COMPANY_NAME = "산림청";
interface AgreementField {
root: HTMLElement;
input: HTMLInputElement;
}
function checkboxWithAccordion(label: string, contentText: string): AgreementField {
const container = document.createElement("div");
container.className = "ui-collapsible is-collapsed a07-register__agreement";
const summary = document.createElement("div");
summary.className = "ui-collapsible__title a07-register__agreement-summary";
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(box.root, labelSpan);
const content = document.createElement("div");
content.className = "a07-register__agreement-content";
content.textContent = contentText;
container.append(summary, content);
attachCollapsible(container);
return { root: container, input: box.input };
}
async function loadAgreementText(filename: string): Promise<string> {
const filePath = `resources/app_policies/${filename}`;
const response = await fetch(filePath);
if (!response.ok) {
console.warn(`Failed to load ${filename}`);
return `[${filename}을 불러올 수 없습니다]`;
}
return await response.text();
}
export function renderA07Register(root: HTMLElement): void {
const page = document.createElement("div");
page.className = "a07-register";
const title = document.createElement("h1");
title.className = "a07-register__title";
title.textContent = L("A07_Register_Title");
const name = createInputField({ label: L("A07_Register_Field_Name"), type: "text" });
const position = createInputField({
label: L("A07_Register_Field_Position"),
type: "text",
});
const email = createInputField({ label: L("A07_Register_Field_Email"), type: "email" });
// 메일 *.go.kr = 회사 「산림청」 고정(요구 347-8) — 서버도 같은 규칙으로 붙임.
const company = createInputField({ label: L("A07_Register_Field_Company"), type: "text" });
company.input.value = FOREST_COMPANY_NAME;
company.input.readOnly = true;
company.root.hidden = true;
email.input.addEventListener("input", () => {
company.root.hidden = !email.input.value.trim().toLowerCase().endsWith(".go.kr");
});
const phone = createInputField({ label: L("A07_Register_Field_Phone"), type: "tel" });
const password = createInputField({ label: L("A07_Register_Field_Password"), type: "password" });
const passwordConfirm = createInputField({
label: L("A07_Register_Field_PasswordConfirm"),
type: "password",
});
const terms: AgreementField = {
root: document.createElement("div"),
input: document.createElement("input"),
};
const privacy: AgreementField = {
root: document.createElement("div"),
input: document.createElement("input"),
};
const marketing: AgreementField = {
root: document.createElement("div"),
input: document.createElement("input"),
};
const otp = createInputField({ label: L("A07_Register_Field_Otp"), type: "text" });
otp.root.hidden = true;
const submit = createButton({ label: L("A07_Register_Submit"), type: "submit" });
const form = document.createElement("form");
form.className = "a07-register__form";
let verifyStep = false;
let termsText = "";
let privacyText = "";
let marketingText = "";
async function loadAgreementTexts(): Promise<void> {
termsText = await loadAgreementText("terms_of_service.txt");
privacyText = await loadAgreementText("privacy_policy.txt");
marketingText = await loadAgreementText("marketing_consent.txt");
const termsField = checkboxWithAccordion(L("A07_Register_Terms"), termsText);
terms.root = termsField.root;
terms.input = termsField.input;
const privacyField = checkboxWithAccordion(L("A07_Register_Privacy"), privacyText);
privacy.root = privacyField.root;
privacy.input = privacyField.input;
const marketingField = checkboxWithAccordion(L("A07_Register_Marketing"), marketingText);
marketing.root = marketingField.root;
marketing.input = marketingField.input;
form.innerHTML = "";
form.append(
name.root,
position.root,
email.root,
company.root,
phone.root,
password.root,
passwordConfirm.root,
terms.root,
privacy.root,
marketing.root,
otp.root,
submit,
);
}
async function onA07_Register_Submit_Click(event: Event): Promise<void> {
event.preventDefault();
showLoadingOverlay();
try {
if (verifyStep) {
await verifyRegistration(email.input.value.trim(), otp.input.value);
showToast(L("A07_Register_Success"), "success");
navigateTo(ROUTES.A06_LOGIN);
return;
}
if (
[name, email, password, passwordConfirm].some((field) => isBlank(field.input.value)) ||
!isValidEmail(email.input.value)
) {
throw new Error(L("A07_Register_Error_Required"));
}
if (!hasMinLength(password.input.value, 8)) throw new Error(L("A07_Register_Error_Password"));
if (password.input.value !== passwordConfirm.input.value)
throw new Error(L("A07_Register_Error_PasswordMismatch"));
if (!terms.input.checked || !privacy.input.checked)
throw new Error(L("A07_Register_Error_Terms"));
await requestRegistration({
email: email.input.value.trim(),
password: password.input.value,
password_confirm: passwordConfirm.input.value,
name: name.input.value.trim(),
position: position.input.value.trim() || null,
phone: phone.input.value.trim() || null,
terms_version: "1.0",
terms_agreed: true,
privacy_agreed: true,
marketing_agreed: marketing.input.checked,
});
verifyStep = true;
Array.from(form.children).forEach((child) => {
if (child !== otp.root && child !== submit) {
(child as HTMLElement).hidden = true;
}
});
otp.root.hidden = false;
submit.hidden = false;
submit.querySelector(".ui-btn__label")!.textContent = L("A07_Register_Verify");
showToast(L("A07_Register_OtpSent"), "info");
} catch (error) {
showToast(error instanceof Error ? error.message : L("A07_Register_Error_Request"), "error");
} finally {
hideLoadingOverlay();
}
}
form.addEventListener("submit", onA07_Register_Submit_Click);
loadAgreementTexts().then(() => {
const login = createButton({
label: L("A07_Register_ToLogin"),
variant: "ghost",
onClick: () => navigateTo(ROUTES.A06_LOGIN),
});
const card = createCard({ body: [title, form, login] });
card.classList.add("a07-register__card");
page.append(card);
root.append(page);
});
}