- B01_Dashboard_Repository 에서 회사·가입신청 부분을 B01_Dashboard_Repository_Company 로 분리 (740줄 -> 508줄) - 팀원 초대 메일 본문의 회사명·이름 HTML 이스케이프 (임의 HTML 삽입 차단) - 지도 타일 엔드포인트 z/x/y 범위 검증 추가 - 회사 정보 수정 시 회사명·사업자번호 중복은 409 로 응답 (기존 500) - 값 변경 없는 회사 수정이 404 로 튕기던 문제 수정 - 팀원 미선택 시 안내 문구 한글화 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
673 lines
24 KiB
TypeScript
673 lines
24 KiB
TypeScript
import { ui_locales, currentLanguageIndex } from "@ui/ui_template_locale";
|
|
import {
|
|
createButton,
|
|
createInputField,
|
|
createSelectField,
|
|
showToast,
|
|
showLoadingOverlay,
|
|
hideLoadingOverlay,
|
|
} from "@ui/ui_template_elements";
|
|
import {
|
|
updateProject,
|
|
deleteProject,
|
|
changeUserRole,
|
|
updateDashboardUser,
|
|
removeCompanyMember,
|
|
createCompany,
|
|
updateCompany,
|
|
joinCompany,
|
|
searchCompanies,
|
|
addCompanyMember,
|
|
searchMemberCandidates,
|
|
inviteMember,
|
|
deleteDashboardUser,
|
|
fetchCompanyMembers,
|
|
fetchCompanyAssets,
|
|
fetchUserCompany,
|
|
updateCompanyAsset,
|
|
createCompanyAsset,
|
|
setCompanyLogo,
|
|
type CompanyInfo,
|
|
type DashboardUser,
|
|
type ProjectItem,
|
|
type Member,
|
|
} from "./B01_Dashboard_Api_Fetch";
|
|
import { createAssetField } from "./B01_Dashboard_UI_AssetPicker";
|
|
import { buildAddressMap } from "./B01_Dashboard_UI_MapPreview";
|
|
import {
|
|
attachModalDismiss,
|
|
buildUserFields,
|
|
type ModalDismissHandle,
|
|
} from "./B01_Dashboard_UI_Common";
|
|
|
|
function L(key: keyof typeof ui_locales): string {
|
|
return ui_locales[key][currentLanguageIndex];
|
|
}
|
|
|
|
function openModal(
|
|
title: string,
|
|
body: HTMLElement[],
|
|
onConfirm: () => Promise<void>,
|
|
options: { extra?: () => string } = {},
|
|
): void {
|
|
const modal = document.createElement("div");
|
|
modal.className = "b01-dashboard__modal";
|
|
const panel = document.createElement("div");
|
|
panel.className = "b01-dashboard__modal-panel";
|
|
const heading = document.createElement("h3");
|
|
heading.className = "b01-dashboard__modal-title";
|
|
heading.textContent = title;
|
|
const actions = document.createElement("div");
|
|
actions.className = "b01-dashboard__actions";
|
|
// 닫는 길은 하나로 — [취소]·바깥 클릭·Esc 모두 같은 변경 확인을 거친다.
|
|
let dismiss: ModalDismissHandle | null = null;
|
|
actions.append(
|
|
createButton({
|
|
label: L("Common_Btn_Cancel"),
|
|
variant: "ghost",
|
|
onClick: () => void dismiss?.tryClose(),
|
|
}),
|
|
createButton({
|
|
label: L("Common_Btn_Confirm"),
|
|
onClick: async () => {
|
|
showLoadingOverlay();
|
|
try {
|
|
await onConfirm();
|
|
modal.remove();
|
|
window.dispatchEvent(new HashChangeEvent("hashchange"));
|
|
} catch (error) {
|
|
showToast(
|
|
error instanceof Error ? error.message : L("B01_Dashboard_RequestFailed"),
|
|
"error",
|
|
);
|
|
} finally {
|
|
hideLoadingOverlay();
|
|
}
|
|
},
|
|
}),
|
|
);
|
|
panel.append(heading, ...body, actions);
|
|
modal.append(panel);
|
|
document.body.append(modal);
|
|
dismiss = attachModalDismiss(modal, panel, { extra: options.extra });
|
|
}
|
|
|
|
export async function openEditProjectModal(
|
|
user: DashboardUser,
|
|
project: ProjectItem,
|
|
): Promise<void> {
|
|
// 참여자로 지정된 일반 사용자는 수정할 수 있다 (2026-09-06 사용자 확정).
|
|
const isUserOnly = user.role === "USER" && !(project.member_user_ids ?? []).includes(user.id);
|
|
// 담당자는 회사 구성원에서, 로고·서명은 회사 공유 자산에서 고른다 (2026-09-02 사용자 확정).
|
|
// 회사 정보는 로고 기본 연결을 보이기 위해 함께 받는다 (2026-09-04 사용자 지시).
|
|
const [members, assets, company] = await Promise.all([
|
|
fetchCompanyMembers(project.company_id),
|
|
fetchCompanyAssets(project.company_id),
|
|
fetchUserCompany().catch(() => null),
|
|
]);
|
|
// 남의 회사 프로젝트(시스템관리자)에서는 내 회사 로고를 기본으로 내밀지 않는다.
|
|
const sameCompany = company != null && company.id === project.company_id;
|
|
const companyLogo =
|
|
(sameCompany &&
|
|
assets.find((asset) => asset.kind === "LOGO" && asset.id === company.logo_asset_id)) ||
|
|
null;
|
|
|
|
const name = createInputField({
|
|
label: L("B01_Dashboard_Table_Project"),
|
|
value: project.name,
|
|
required: true,
|
|
});
|
|
const region = createInputField({
|
|
label: L("B01_Dashboard_Table_Region"),
|
|
value: project.region ?? "",
|
|
});
|
|
const roadType = createInputField({ label: "임도 종류", value: project.road_type ?? "" });
|
|
const year = createInputField({
|
|
label: "사업 연도",
|
|
type: "number",
|
|
value: String(project.project_year ?? ""),
|
|
});
|
|
const length = createInputField({
|
|
label: "예상 연장 (m)",
|
|
type: "number",
|
|
value: String(project.estimated_length_m ?? ""),
|
|
});
|
|
// 계획노선 사용 범위 — 등록(B02)에서 받은 값을 여기서도 고친다 (2026-09-04 사용자 지시).
|
|
const routeStart = createInputField({
|
|
label: "노선 시작 누가거리 (m)",
|
|
type: "number",
|
|
value: project.route_start_m == null ? "" : String(project.route_start_m),
|
|
placeholder: "비우면 처음부터",
|
|
});
|
|
const routeEnd = createInputField({
|
|
label: "노선 종료 누가거리 (m)",
|
|
type: "number",
|
|
value: project.route_end_m == null ? "" : String(project.route_end_m),
|
|
placeholder: "비우면 끝까지",
|
|
});
|
|
const memo = createInputField({ label: "비고", value: project.memo ?? "" });
|
|
// 도면 표제란·표지에 그대로 실리는 값 — 프로그램이 지어낼 수 없어 여기서 받는다.
|
|
const clientOrg = createInputField({
|
|
label: "시행청 (도면 표제란)",
|
|
value: project.client_org ?? "",
|
|
});
|
|
const projectNumber = createInputField({
|
|
label: "연도·기번 (표지)",
|
|
value: project.project_number ?? "",
|
|
placeholder: "예: 2026년 간선임도(기번3-울진.대흥)",
|
|
});
|
|
const workAmount = createInputField({
|
|
label: "사업량 (표지)",
|
|
value: project.work_amount ?? "",
|
|
placeholder: "예: L=2.14km",
|
|
});
|
|
// 설계일자는 확정일 자동이 아니라 사용자가 지정한다 (2026-09-02 사용자 확정).
|
|
const designDate = createInputField({
|
|
label: "설계일자 (도면 표제란)",
|
|
type: "date",
|
|
value: (project.design_date ?? "").slice(0, 10),
|
|
});
|
|
const memberText = (member: Member) =>
|
|
member.position ? `${member.name} (${member.position})` : member.name;
|
|
const personOptions = [
|
|
{ value: "", text: "(미지정)" },
|
|
...members.map((member) => ({ value: String(member.id), text: memberText(member) })),
|
|
];
|
|
const persons: HTMLSelectElement[] = [];
|
|
// 담당자는 이미 등록된 팀원 중에서만 고른다 (2026-09-06 사용자 확정) —
|
|
// 이 자리에서 계정을 만드는 「신규 등록…」은 없앴다.
|
|
const person = (label: string, current: number | null | undefined) => {
|
|
const field = createSelectField({
|
|
label,
|
|
options: personOptions,
|
|
value: String(current ?? ""),
|
|
});
|
|
persons.push(field.select);
|
|
return field;
|
|
};
|
|
const pm = person("과업책임자 (도면 표제란)", project.pm_user_id);
|
|
const fieldLead = person("분야별책임자 (도면 표제란)", project.field_lead_user_id);
|
|
const designer = person("설계자 (도면 표제란)", project.designer_user_id);
|
|
const logo = createAssetField(
|
|
"회사 로고 (도면 표제란)",
|
|
"LOGO",
|
|
assets,
|
|
project.logo_asset_id,
|
|
project.company_id,
|
|
user,
|
|
{
|
|
// 프로젝트가 안 고르면 도면에는 회사 로고가 실린다 — 저장값은 계속 비워 둬 연결을 유지한다.
|
|
fallback: sameCompany
|
|
? {
|
|
asset: companyLogo,
|
|
prefix: "회사 기본 로고",
|
|
missingNote: "(없음) — 회사 로고 미지정 (회사 정보 화면의 「로고 지정…」)",
|
|
}
|
|
: undefined,
|
|
},
|
|
);
|
|
if (isUserOnly) {
|
|
name.input.disabled = true;
|
|
region.input.disabled = true;
|
|
roadType.input.disabled = true;
|
|
year.input.disabled = true;
|
|
length.input.disabled = true;
|
|
routeStart.input.disabled = true;
|
|
routeEnd.input.disabled = true;
|
|
memo.input.disabled = true;
|
|
clientOrg.input.disabled = true;
|
|
projectNumber.input.disabled = true;
|
|
workAmount.input.disabled = true;
|
|
designDate.input.disabled = true;
|
|
pm.select.disabled = true;
|
|
fieldLead.select.disabled = true;
|
|
designer.select.disabled = true;
|
|
}
|
|
|
|
// 칸이 많아 두 줄 격자로 — 첫 칸(공사명)만 가로로 다 쓴다.
|
|
const grid = document.createElement("div");
|
|
grid.className = "b01-dashboard__form-grid";
|
|
grid.append(
|
|
name.root,
|
|
region.root,
|
|
roadType.root,
|
|
year.root,
|
|
length.root,
|
|
routeStart.root,
|
|
routeEnd.root,
|
|
memo.root,
|
|
clientOrg.root,
|
|
projectNumber.root,
|
|
workAmount.root,
|
|
designDate.root,
|
|
pm.root,
|
|
fieldLead.root,
|
|
designer.root,
|
|
logo.root,
|
|
);
|
|
// 참여자 — 도면 표제란 3역할과 별개로 설계에 손대는 사람들 (2026-09-06 사용자 확정).
|
|
const memberBox = document.createElement("div");
|
|
memberBox.className = "b01-dashboard__members";
|
|
const memberLabel = document.createElement("p");
|
|
memberLabel.className = "b01-dashboard__modal-text";
|
|
memberLabel.textContent = "참여자 (고른 사람은 이 프로젝트를 수정할 수 있음)";
|
|
memberBox.append(memberLabel);
|
|
const memberChecks: HTMLInputElement[] = [];
|
|
for (const member of members) {
|
|
const row = document.createElement("label");
|
|
row.className = "b01-dashboard__member-row";
|
|
const check = document.createElement("input");
|
|
check.type = "checkbox";
|
|
check.value = String(member.id);
|
|
check.checked = (project.member_user_ids ?? []).includes(member.id);
|
|
check.disabled = isUserOnly;
|
|
memberChecks.push(check);
|
|
row.append(check, document.createTextNode(` ${memberText(member)}`));
|
|
memberBox.append(row);
|
|
}
|
|
|
|
const userId = (select: HTMLSelectElement) => (select.value ? Number(select.value) : null);
|
|
|
|
// 로고는 입력칸이 아니라 고르기 모달로 바뀌므로 변경 판정에 따로 실어 준다.
|
|
const editProjectExtra = (): string => String(logo.value() ?? "");
|
|
|
|
openModal(
|
|
L("B01_Dashboard_EditProject"),
|
|
[grid, memberBox],
|
|
async () => {
|
|
await updateProject(project.id, {
|
|
name: name.input.value.trim(),
|
|
region: region.input.value.trim() || null,
|
|
road_type: roadType.input.value.trim() || null,
|
|
project_year: year.input.value ? Number(year.input.value) : null,
|
|
estimated_length_m: length.input.value ? Number(length.input.value) : null,
|
|
route_start_m: routeStart.input.value ? Number(routeStart.input.value) : null,
|
|
route_end_m: routeEnd.input.value ? Number(routeEnd.input.value) : null,
|
|
memo: memo.input.value.trim() || null,
|
|
status: project.status,
|
|
client_org: clientOrg.input.value.trim() || null,
|
|
project_number: projectNumber.input.value.trim() || null,
|
|
work_amount: workAmount.input.value.trim() || null,
|
|
design_date: designDate.input.value || null,
|
|
pm_user_id: userId(pm.select),
|
|
field_lead_user_id: userId(fieldLead.select),
|
|
designer_user_id: userId(designer.select),
|
|
logo_asset_id: logo.value(),
|
|
// 서명은 사람 계정에 붙는다 (2026-09-02 사용자 확정) — 프로젝트는 더 고르지 않는다.
|
|
signature_asset_id: null,
|
|
member_user_ids: memberChecks.filter((c) => c.checked).map((c) => Number(c.value)),
|
|
});
|
|
showToast(L("B01_Dashboard_Saved"), "success");
|
|
},
|
|
{ extra: editProjectExtra },
|
|
);
|
|
}
|
|
|
|
export function openDeleteProjectModal(user: DashboardUser, project: ProjectItem): void {
|
|
const warning = document.createElement("p");
|
|
warning.className = "b01-dashboard__modal-text";
|
|
// 하드 삭제 모드에서는 업로드 원본까지 사라진다 — 문구를 바꿔 실수로 날리는 걸 막는다.
|
|
warning.textContent = user.project_delete_hard
|
|
? L("B01_Dashboard_Confirm_DeleteProject_Hard")
|
|
: L("B01_Dashboard_Confirm_DeleteProject");
|
|
|
|
openModal(L("B01_Dashboard_DeleteProject"), [warning], async () => {
|
|
await deleteProject(project.id);
|
|
showToast(L("B01_Dashboard_Saved"), "success");
|
|
});
|
|
}
|
|
|
|
export function openEditUserModal(user: DashboardUser, target: Member | DashboardUser): void {
|
|
// 기본정보 폼과 같은 칸 한 벌을 쓴다 (2026-09-06 사용자 지시).
|
|
const fields = buildUserFields({
|
|
name: target.name,
|
|
email: target.email,
|
|
position: target.position,
|
|
department: target.department,
|
|
phone: (target as DashboardUser).phone,
|
|
});
|
|
const isAdmin = user.role === "SYSTEM_ADMIN" || user.role === "ADMIN";
|
|
// 관리자는 계정을 비활성화할 수 있다 (2026-09-06 사용자 지시) — 비활성 계정은 다음
|
|
// 요청에서 세션이 끊긴다.
|
|
const statusField = createSelectField({
|
|
label: L("B01_Dashboard_Table_Status"),
|
|
options: [
|
|
{ value: "ACTIVE", text: "활성" },
|
|
{ value: "INACTIVE", text: "비활성" },
|
|
],
|
|
value: target.status === "INACTIVE" ? "INACTIVE" : "ACTIVE",
|
|
});
|
|
|
|
// 서명은 사람에게 붙는다 (2026-09-02 사용자 확정) — 도면 표제란이 이 사람 자리를
|
|
// 채울 때 그대로 실린다. 고르는 즉시 그 사람에게 물린다.
|
|
const signatureSlot = document.createElement("div");
|
|
const companyId = (target as DashboardUser).company_id ?? user.company_id;
|
|
void fetchCompanyAssets(companyId).then((assets) => {
|
|
const owned = assets.find((asset) => asset.kind === "SIGNATURE" && asset.user_id === target.id);
|
|
signatureSlot.append(
|
|
createAssetField("서명", "SIGNATURE", assets, owned?.id ?? null, companyId, user, {
|
|
owner: { id: target.id, name: target.name },
|
|
onChange: async (assetId) => {
|
|
if (assetId === null) return;
|
|
const picked = assets.find((asset) => asset.id === assetId);
|
|
if (picked)
|
|
await updateCompanyAsset(assetId, { label: picked.label, user_id: target.id });
|
|
},
|
|
}).root,
|
|
);
|
|
});
|
|
|
|
const rows = isAdmin
|
|
? [fields.grid, statusField.root, signatureSlot]
|
|
: [fields.grid, signatureSlot];
|
|
openModal(L("B01_Dashboard_EditUser"), rows, async () => {
|
|
if (!fields.validate()) return;
|
|
await updateDashboardUser(target.id, {
|
|
...fields.values(),
|
|
// 상태는 관리자만 바꾼다. 그 밖에는 지금 값을 그대로 되돌려 보낸다.
|
|
status: isAdmin ? statusField.select.value : target.status,
|
|
});
|
|
showToast(L("B01_Dashboard_Saved"), "success");
|
|
});
|
|
}
|
|
|
|
export function openChangeRoleModal(target: Member | DashboardUser): void {
|
|
const roleField = createSelectField({
|
|
label: L("B01_Dashboard_Table_Role"),
|
|
options: [
|
|
{ value: "USER", text: "USER" },
|
|
{ value: "ADMIN", text: "ADMIN" },
|
|
],
|
|
value: target.role,
|
|
});
|
|
|
|
openModal(L("B01_Dashboard_ChangeRole"), [roleField.root], async () => {
|
|
await changeUserRole(target.id, roleField.select.value);
|
|
showToast(L("B01_Dashboard_Saved"), "success");
|
|
});
|
|
}
|
|
|
|
export function openDeleteUserModal(target: Member | DashboardUser): void {
|
|
// 두 가지가 다르다 — 회사에서 빼면 계정은 남고(무소속), 계정 삭제는 로그인 자체가 막힌다
|
|
// (2026-09-06 사용자 확정).
|
|
const mode = createSelectField({
|
|
label: "처리 방식",
|
|
options: [
|
|
{ value: "REMOVE", text: "회사에서 빼기 (계정은 남음)" },
|
|
{ value: "DELETE", text: "계정 삭제 (로그인 불가)" },
|
|
],
|
|
value: "REMOVE",
|
|
});
|
|
const warning = document.createElement("p");
|
|
warning.className = "b01-dashboard__modal-text";
|
|
warning.textContent = L("B01_Dashboard_Confirm_DeleteUser");
|
|
|
|
openModal(L("B01_Dashboard_DeleteUser"), [mode.root, warning], async () => {
|
|
if (mode.select.value === "DELETE") await deleteDashboardUser(target.id);
|
|
else await removeCompanyMember(target.id);
|
|
showToast(L("B01_Dashboard_Saved"), "success");
|
|
});
|
|
}
|
|
|
|
/** 회사 등록·수정이 함께 쓰는 입력칸 — 순서는 사업자등록번호 > 회사명 > 대표자명 > 로고 > 주소. */
|
|
function companyFields(company?: CompanyInfo): {
|
|
rows: HTMLElement[];
|
|
values: () => {
|
|
name: string;
|
|
business_registration_number: string;
|
|
business_address: string | null;
|
|
business_owner: string | null;
|
|
};
|
|
logoFile: () => File | undefined;
|
|
valid: () => boolean;
|
|
} {
|
|
const number = createInputField({
|
|
label: L("B01_Dashboard_Field_BusinessNumber"),
|
|
value: company?.business_registration_number ?? "",
|
|
required: true,
|
|
});
|
|
const name = createInputField({
|
|
label: L("B01_Dashboard_Table_Company"),
|
|
value: company?.name ?? "",
|
|
required: true,
|
|
});
|
|
const owner = createInputField({
|
|
label: L("B01_Dashboard_Field_Owner"),
|
|
value: company?.business_owner ?? "",
|
|
});
|
|
const logo = createInputField({ label: "회사 로고 (png·jpg·webp·svg, 2MB 이하)" });
|
|
logo.input.type = "file";
|
|
logo.input.accept = ".png,.jpg,.jpeg,.webp,.svg";
|
|
const address = createInputField({
|
|
label: L("B01_Dashboard_Field_Address"),
|
|
value: company?.business_address ?? "",
|
|
});
|
|
// 주소가 맞는 자리인지 지도로 확인한다 (2026-09-06 사용자 지시).
|
|
const map = buildAddressMap();
|
|
const mapBtn = createButton({
|
|
label: "지도 확인",
|
|
variant: "ghost",
|
|
onClick: async function onB01_Company_Map_Click() {
|
|
await map.show(address.input.value.trim());
|
|
},
|
|
});
|
|
const addressRow = document.createElement("div");
|
|
addressRow.append(address.root, mapBtn, map.root);
|
|
|
|
return {
|
|
rows: [number.root, name.root, owner.root, logo.root, addressRow],
|
|
values: () => ({
|
|
name: name.input.value.trim(),
|
|
business_registration_number: number.input.value.trim(),
|
|
business_address: address.input.value.trim() || null,
|
|
business_owner: owner.input.value.trim() || null,
|
|
}),
|
|
logoFile: () => logo.input.files?.[0],
|
|
valid: () => Boolean(name.input.value.trim() && number.input.value.trim()),
|
|
};
|
|
}
|
|
|
|
export function openCreateCompanyModal(): void {
|
|
const fields = companyFields();
|
|
// 같은 회사를 두 번 만들지 않게, 등록 전에 이미 있는 회사를 찾아 보여 준다
|
|
// (2026-09-06 사용자 지시).
|
|
const matches = document.createElement("div");
|
|
matches.className = "b01-dashboard__actions";
|
|
const findBtn = createButton({
|
|
label: "이미 있는 회사인지 찾기",
|
|
variant: "ghost",
|
|
onClick: async function onB01_Company_Duplicate_Click() {
|
|
matches.innerHTML = "";
|
|
const values = fields.values();
|
|
const query = values.business_registration_number || values.name;
|
|
if (!query) return;
|
|
const found = await searchCompanies(query);
|
|
if (found.length === 0) {
|
|
const empty = document.createElement("p");
|
|
empty.className = "b01-dashboard__modal-text";
|
|
empty.textContent = "같은 회사가 없습니다. 그대로 등록하십시오.";
|
|
matches.append(empty);
|
|
return;
|
|
}
|
|
for (const company of found) {
|
|
matches.append(
|
|
createButton({
|
|
label: `${company.name} — ${L("B01_Dashboard_JoinCompany")}`,
|
|
variant: "ghost",
|
|
onClick: async () => {
|
|
showLoadingOverlay();
|
|
try {
|
|
await joinCompany(company.id);
|
|
showToast(L("B01_Dashboard_Saved"), "success");
|
|
} catch {
|
|
showToast("요청 실패", "error");
|
|
} finally {
|
|
hideLoadingOverlay();
|
|
}
|
|
},
|
|
}),
|
|
);
|
|
}
|
|
},
|
|
});
|
|
|
|
openModal(
|
|
L("B01_Dashboard_Modal_CreateCompany"),
|
|
[...fields.rows, findBtn, matches],
|
|
async () => {
|
|
if (!fields.valid()) return;
|
|
const values = fields.values();
|
|
const created = await createCompany(values);
|
|
const file = fields.logoFile();
|
|
if (file && created?.company_id) {
|
|
const form = new FormData();
|
|
form.append("kind", "LOGO");
|
|
form.append("label", `${values.name} 로고`);
|
|
form.append("file", file);
|
|
form.append("company_id", String(created.company_id));
|
|
const assetId = await createCompanyAsset(form);
|
|
await setCompanyLogo(assetId, created.company_id);
|
|
}
|
|
showToast(L("B01_Dashboard_Saved"), "success");
|
|
},
|
|
);
|
|
}
|
|
|
|
/** 회사 정보 수정 (2026-09-06 사용자 지시) — 회사 관리자는 자기 회사, 시스템관리자는 전체. */
|
|
export function openEditCompanyModal(company: CompanyInfo): void {
|
|
const fields = companyFields(company);
|
|
openModal("회사 정보 수정", fields.rows, async () => {
|
|
if (!fields.valid()) return;
|
|
const values = fields.values();
|
|
await updateCompany(values, company.id);
|
|
const file = fields.logoFile();
|
|
if (file) {
|
|
const form = new FormData();
|
|
form.append("kind", "LOGO");
|
|
form.append("label", `${values.name} 로고`);
|
|
form.append("file", file);
|
|
form.append("company_id", String(company.id));
|
|
const assetId = await createCompanyAsset(form);
|
|
await setCompanyLogo(assetId, company.id);
|
|
}
|
|
showToast(L("B01_Dashboard_Saved"), "success");
|
|
});
|
|
}
|
|
|
|
export function openFindCompanyModal(): void {
|
|
const query = createInputField({ label: L("B01_Dashboard_Field_Search"), required: true });
|
|
const results = document.createElement("div");
|
|
results.className = "b01-dashboard__actions";
|
|
const search = createButton({
|
|
label: L("Common_Btn_Search"),
|
|
variant: "ghost",
|
|
onClick: async function onB01_Company_Search_Click() {
|
|
results.innerHTML = "";
|
|
const companies = await searchCompanies(query.input.value.trim());
|
|
for (const company of companies) {
|
|
results.append(
|
|
createButton({
|
|
label: `${company.name} ${L("B01_Dashboard_JoinCompany")}`,
|
|
variant: "ghost",
|
|
onClick: async () => {
|
|
showLoadingOverlay();
|
|
try {
|
|
await joinCompany(company.id);
|
|
showToast(L("B01_Dashboard_Saved"), "success");
|
|
} catch (e) {
|
|
showToast("요청 실패", "error");
|
|
} finally {
|
|
hideLoadingOverlay();
|
|
}
|
|
},
|
|
}),
|
|
);
|
|
}
|
|
},
|
|
});
|
|
openModal(
|
|
L("B01_Dashboard_Modal_FindCompany"),
|
|
[query.root, search, results],
|
|
async () => undefined,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 팀원 등록 — 이미 가입한 사람 중 **소속이 없는 사람**만 골라 붙인다
|
|
* (2026-09-06 사용자 확정). 계정을 대신 만들지 않는다. 아직 가입하지 않은 사람에게는
|
|
* 안내 메일만 보낸다.
|
|
*/
|
|
export function openAddMemberModal(onCreated?: (member: Member) => void): void {
|
|
const query = createInputField({
|
|
label: "이름 또는 이메일로 찾기",
|
|
placeholder: "두 글자 이상",
|
|
});
|
|
const results = document.createElement("div");
|
|
results.className = "b01-dashboard__actions";
|
|
let picked: Member | null = null;
|
|
|
|
const search = createButton({
|
|
label: L("Common_Btn_Search"),
|
|
variant: "ghost",
|
|
onClick: async function onB01_Member_Search_Click() {
|
|
results.innerHTML = "";
|
|
const text = query.input.value.trim();
|
|
if (text.length < 2) {
|
|
query.setError("두 글자 이상 입력하십시오.");
|
|
return;
|
|
}
|
|
query.setError();
|
|
const users = await searchMemberCandidates(text);
|
|
if (users.length === 0) {
|
|
const empty = document.createElement("p");
|
|
empty.className = "b01-dashboard__modal-text";
|
|
empty.textContent = "소속 없는 가입자가 없습니다. 아직 가입 전이면 안내 메일을 보내십시오.";
|
|
results.append(empty);
|
|
return;
|
|
}
|
|
for (const candidate of users) {
|
|
results.append(
|
|
createButton({
|
|
label: `${candidate.name} (${candidate.email})`,
|
|
variant: picked?.id === candidate.id ? "filled" : "ghost",
|
|
onClick: () => {
|
|
picked = candidate;
|
|
query.input.value = `${candidate.name} (${candidate.email})`;
|
|
results.innerHTML = "";
|
|
},
|
|
}),
|
|
);
|
|
}
|
|
},
|
|
});
|
|
|
|
const invite = createButton({
|
|
label: "가입 안내 메일 보내기",
|
|
variant: "ghost",
|
|
onClick: async function onB01_Member_Invite_Click() {
|
|
const text = query.input.value.trim();
|
|
if (!text.includes("@")) {
|
|
query.setError("메일을 보낼 이메일 주소를 입력하십시오.");
|
|
return;
|
|
}
|
|
query.setError();
|
|
await inviteMember(text);
|
|
showToast("안내 메일을 보냈습니다.", "success");
|
|
},
|
|
});
|
|
|
|
const bar = document.createElement("div");
|
|
bar.className = "b01-dashboard__actions";
|
|
bar.append(search, invite);
|
|
|
|
openModal(L("B01_Dashboard_Modal_AddMember"), [query.root, bar, results], async () => {
|
|
if (!picked) {
|
|
query.setError("등록할 사람을 먼저 고르십시오.");
|
|
throw new Error("등록할 사람을 먼저 고르십시오.");
|
|
}
|
|
const created = await addCompanyMember(picked.id);
|
|
showToast(L("B01_Dashboard_Saved"), "success");
|
|
if (created?.member) onCreated?.(created.member);
|
|
});
|
|
}
|