/* Main — 방사형 허브 + 배치 변형 */
/* ---- 업무영역 카드 내용 편집 저장소 (세이프스쿨 전용) ----------------
기본값은 SS_DATA.AREAS, 편집분만 localStorage에 덮어쓰기로 보관 */
const SS_AREAS_KEY = "ss-areas-override-v1";
function loadAreaOverrides() {
try { return JSON.parse(localStorage.getItem(SS_AREAS_KEY)) || {}; }
catch (e) { return {}; }
}
function saveAreaOverrides(ov) {
try { localStorage.setItem(SS_AREAS_KEY, JSON.stringify(ov)); } catch (e) {}
}
function mergeAreas(overrides) {
return window.SS_DATA.AREAS.map((a) => {
const o = overrides[a.id];
return o ? { ...a, ...o } : a;
});
}
/* ---- 카드별 보조 박스 (세이프스쿨이 사진/안내 입력 → 학교가 열람) ----
내용이 비면 학교·미리보기에서는 아예 보이지 않음 */
const SS_EXTRA_KEY = "ss-area-extra-v1";
function loadAreaExtras() {
try { return JSON.parse(localStorage.getItem(SS_EXTRA_KEY)) || {}; }
catch (e) { return {}; }
}
function saveAreaExtras(ex) {
try { localStorage.setItem(SS_EXTRA_KEY, JSON.stringify(ex)); } catch (e) {}
}
// 업로드 사진을 캔버스로 축소해 dataURL로 (localStorage 용량 보호)
function fileToScaledDataURL(file, maxW, cb) {
const reader = new FileReader();
reader.onload = () => {
const img = new Image();
img.onload = () => {
const scale = Math.min(1, maxW / img.width);
const w = Math.round(img.width * scale), h = Math.round(img.height * scale);
const cv = document.createElement("canvas");
cv.width = w; cv.height = h;
cv.getContext("2d").drawImage(img, 0, 0, w, h);
cb(cv.toDataURL("image/jpeg", 0.82));
};
img.src = reader.result;
};
reader.readAsDataURL(file);
}
function AreaExtra({ extra, editing, onText, onPhoto, onClearPhoto }) {
const fileRef = useRef(null);
const ex = extra || {};
const hasContent = !!(ex.text || ex.img);
const handleFile = (e) => {
const f = e.target.files && e.target.files[0];
if (!f) return;
fileToScaledDataURL(f, 720, (dataURL) => onPhoto(dataURL));
e.target.value = "";
};
// 편집 모드가 아니면 — 내용이 있을 때만 노출 (학교 담당자 / 세이프스쿨 미리보기)
if (!editing) {
if (!hasContent) return null;
return (
{ex.img &&

}
{ex.text &&
{ex.text}
}
);
}
// 편집 모드 (세이프스쿨 전용) — 항상 입력 affordance 표시
return (
e.stopPropagation()}>
학교 안내 (선택)
{ex.img && (
)}
);
}
function AreaInner({ area, badge, editing, onField }) {
return (
0 ? "" : " is-empty")} title={"미확인 로그 " + badge + "건"}>{badge > 99 ? "99+" : badge}
{editing ? (
e.stopPropagation()}>
onField("title", e.target.value)} />
onField("sub", e.target.value)} />
) : (
{(area.no ? area.no + ". " : "") + (area.sub ? area.title + " " + area.sub : area.title)}
{area.desc}
)}
{area.active
? 바로가기
: 준비중}
);
}
function BrandCenter({ variant, canAdmin, locked, onClick }) {
const C = window.SS_DATA.COMPANY;
const A = window.SS_DATA.ADMIN;
const arrow = ;
const lock = ;
const base = variant === "radial" ? "radial__center" : "hub-center-card";
const cls = base + (canAdmin ? " is-btn" : "") + (locked ? " is-locked" : "");
const foot = canAdmin
? 관리자 대시보드 {arrow}
: locked
? {lock} 세이프스쿨 전용
: {C.tagline};
return (
);
}
function MainHub({ onOpenArea, onCenterClick, role, layout, loggedIn, unread, hireNew, toast }) {
useLucideSS();
const C = window.SS_DATA.COMPANY;
const canAdmin = loggedIn && role === "safe";
// ---- 업무영역 카드 편집 (세이프스쿨 전용) ----
const [overrides, setOverrides] = useState(loadAreaOverrides);
const [editing, setEditing] = useState(false);
const AREAS = mergeAreas(overrides);
// 권한을 잃으면 편집 모드 자동 종료 (학교 역할/로그아웃)
useEffect(() => { if (!canAdmin && editing) setEditing(false); }, [canAdmin, editing]);
const updateField = (id, field, value) => {
setOverrides((prev) => {
const base = window.SS_DATA.AREAS.find((a) => a.id === id) || {};
const next = { ...prev, [id]: { ...prev[id], [field]: value } };
// 기본값과 동일해지면 해당 오버라이드 정리
const merged = { title: base.title, sub: base.sub, desc: base.desc, ...next[id] };
if (merged.title === base.title && merged.sub === base.sub && merged.desc === base.desc) {
delete next[id];
} else {
next[id] = merged;
}
saveAreaOverrides(next);
return next;
});
};
const resetAreas = () => { setOverrides({}); saveAreaOverrides({}); if (toast) toast("업무영역 카드를 기본값으로 되돌렸습니다"); };
const finishEditing = () => { setEditing(false); if (toast) toast("업무영역 카드 내용이 저장되었습니다"); };
// ---- 카드별 보조 박스 (세이프스쿨 입력 → 학교 열람) ----
const [extras, setExtras] = useState(loadAreaExtras);
const updateExtra = (id, field, value) => {
setExtras((prev) => {
const cur = { ...(prev[id] || {}) };
if (value) cur[field] = value; else delete cur[field];
const next = { ...prev };
if (cur.text || cur.img) next[id] = cur; else delete next[id];
saveAreaExtras(next);
return next;
});
};
const openIf = (a) => { if (editing) return; if (a.active) onOpenArea(a); };
const cls = (a) => "area " + (a.active ? "is-active" : "is-disabled") + (editing ? " is-editing" : "");
const badgeOf = (a) => {
if (!loggedIn) return 0;
if (a.id === "facility") return unread || 0;
if (a.id === "substitute") return hireNew || 0;
return 0;
};
const centerProps = { canAdmin, locked: !canAdmin, onClick: onCenterClick };
// 카드 + 보조 박스를 한 묶음(.area-cell)으로 렌더 — 모든 레이아웃 공용
// sideClass: 보조 박스를 카드의 어느 방향에 둘지 (방사형에서 중앙 원/이웃을 안 가리게)
const renderCell = (a, style, sideClass) => (
openIf(a)}>
updateField(a.id, f, v)} />
updateExtra(a.id, "text", v)}
onPhoto={(d) => updateExtra(a.id, "img", d)}
onClearPhoto={() => updateExtra(a.id, "img", "")} />
);
// 방사형 — 카드 위치(시계 12시부터 시계방향)와 보조 박스 방향
// 0 위, 1·2 오른쪽, 3 아래, 4·5 왼쪽
const RADIAL_SIDE = ["box-top", "box-right", "box-right", "box-bottom", "box-left", "box-left"];
// ---- 방사형 위치 계산 ---- (보조 박스가 바깥으로 뻗을 공간을 위해 반경 축소)
const positions = AREAS.map((_, i) => {
const ang = (-90 + i * 60) * Math.PI / 180;
return { x: 50 + 26 * Math.cos(ang), y: 50 + 24 * Math.sin(ang) };
});
return (
{canAdmin && (
{editing ? (
카드 내용·학교 안내를 수정하세요
) : (
세이프스쿨 전용 — 카드 내용 편집
)}
)}
{layout === "radial" && (
{AREAS.map((a, i) => renderCell(a, { left: positions[i].x + "%", top: positions[i].y + "%" }, RADIAL_SIDE[i]))}
)}
{layout === "columns" && (
{[0, 1, 2].map((i) => renderCell(AREAS[i]))}
{[3, 4, 5].map((i) => renderCell(AREAS[i]))}
)}
{layout === "stack" && (
{[0, 1, 2].map((i) => renderCell(AREAS[i]))}
{[3, 4, 5].map((i) => renderCell(AREAS[i]))}
)}
{C.address}
Tel {C.tel}
FAX {C.fax}
{C.email}
);
}
window.MainHub = MainHub;
window.AreaExtra = AreaExtra;
window.fileToScaledDataURL = fileToScaledDataURL;