Files
DDD1542 3eda684787
Build & Deploy to K8s / build-and-deploy (push) Successful in 4m22s
사용자 대시보드 기능강화 및 인비온 스튜디오 메뉴관리 자잘한수정
2026-04-22 18:27:06 +09:00

346 lines
12 KiB
TypeScript

"use client";
import React, { useEffect, useState } from "react";
import { toast } from "sonner";
import { menuApi } from "@/lib/api/menu";
import type { MenuItem, MenuFormData, LangKey } from "@/lib/api/menu";
import { companyAPI } from "@/lib/api/company";
import { MenuIconPicker } from "@/components/admin/MenuIconPicker";
interface Props {
menu: MenuItem;
parentName: string;
onSaved: () => void;
onDelete: (menuId: string) => void;
onOpenAdvanced: (menuId: string) => void;
}
interface CompanyOption {
company_code: string;
company_name: string;
status?: string;
}
interface FormState {
menuNameKor: string;
menuUrl: string;
menuDesc: string;
menuIcon: string;
seq: number;
companyCode: string;
status: string;
langKey: string;
}
const toForm = (m: MenuItem): FormState => ({
menuNameKor: m.menu_name_kor ?? m.MENU_NAME_KOR ?? "",
menuUrl: m.menu_url ?? m.MENU_URL ?? "",
menuDesc: m.menu_desc ?? m.MENU_DESC ?? "",
menuIcon: m.menu_icon ?? m.MENU_ICON ?? "",
seq: Number(m.seq ?? m.SEQ ?? 1),
companyCode: m.company_code ?? m.COMPANY_CODE ?? "*",
// DB는 소문자 'active'/'inactive' — 원본 그대로 유지
status: (m.status ?? m.STATUS ?? "active").toString().toLowerCase(),
langKey: m.lang_key ?? m.LANG_KEY ?? "",
});
export function MenuSettingsPanel({ menu, parentName, onSaved, onDelete, onOpenAdvanced }: Props) {
const menuId = String(menu.objid ?? menu.OBJID ?? "");
const initial = toForm(menu);
const [form, setForm] = useState<FormState>(initial);
const [saving, setSaving] = useState(false);
// 드롭다운 옵션 소스
const [companies, setCompanies] = useState<CompanyOption[]>([]);
const [langKeys, setLangKeys] = useState<LangKey[]>([]);
useEffect(() => {
setForm(toForm(menu));
}, [menu]);
// 회사 목록 로드 (1회)
useEffect(() => {
let alive = true;
(async () => {
try {
const list = await companyAPI.getList({ status: "active" });
if (alive) setCompanies(list || []);
} catch {
/* silent - 기본값 사용 */
}
})();
return () => {
alive = false;
};
}, []);
// 다국어 키 로드 (companyCode 변경 시 재로드, 활성 키만 필터)
useEffect(() => {
let alive = true;
(async () => {
try {
const resp = await menuApi.getLangKeys({
companyCode: form.companyCode || "*",
});
if (alive && resp.success && resp.data) {
setLangKeys(resp.data.filter((k) => k.isActive === "Y"));
}
} catch {
if (alive) setLangKeys([]);
}
})();
return () => {
alive = false;
};
}, [form.companyCode]);
const isDirty =
form.menuNameKor !== initial.menuNameKor ||
form.menuUrl !== initial.menuUrl ||
form.menuDesc !== initial.menuDesc ||
form.menuIcon !== initial.menuIcon ||
form.seq !== initial.seq ||
form.companyCode !== initial.companyCode ||
form.status !== initial.status ||
form.langKey !== initial.langKey;
const set = <K extends keyof FormState>(k: K, v: FormState[K]) =>
setForm((f) => ({ ...f, [k]: v }));
const handleSave = async () => {
if (!form.menuNameKor.trim()) {
toast.error("메뉴명을 입력하세요");
return;
}
setSaving(true);
try {
const payload: MenuFormData = {
objid: menuId,
parentObjId: String(menu.parent_obj_id ?? menu.PARENT_OBJ_ID ?? "0"),
menuNameKor: form.menuNameKor,
menuUrl: form.menuUrl,
menuDesc: form.menuDesc,
seq: form.seq,
menu_type: String(menu.menu_type ?? menu.MENU_TYPE ?? "1"),
status: form.status,
company_code: form.companyCode,
langKey: form.langKey || undefined,
menuIcon: form.menuIcon || undefined,
};
const resp = await menuApi.updateMenu(menuId, payload);
if (resp.success) {
toast.success(resp.message || "저장되었습니다");
onSaved();
} else {
toast.error(resp.message || "저장 실패");
}
} catch (e) {
toast.error("저장 중 오류가 발생했습니다");
} finally {
setSaving(false);
}
};
const statusOn = form.status === "active";
return (
<div className="v5-mm-pane on v5-mm-pane-wrap">
<div className="v5-mm-sv-hero">
<div className="v5-mm-sv-hero-top">
<div className="v5-mm-sv-hero-ico">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M4 6h16M4 12h16M4 18h10" />
</svg>
</div>
<div className="v5-mm-sv-hero-info">
<div className="v5-mm-sv-hero-path">
/ <b>{parentName || "최상위"}</b>
</div>
<h2>
{form.menuNameKor || "(이름 없음)"}
<span className={`v5-mm-chip${statusOn ? " on" : ""}`}>{statusOn ? "Active" : "Inactive"}</span>
<span className="v5-mm-chip scope">{form.companyCode === "*" ? "공용 *" : form.companyCode || "—"}</span>
<span className="v5-mm-chip">L{Number(menu.lev ?? menu.LEV ?? 1)}</span>
</h2>
</div>
</div>
<div className="v5-mm-sv-hero-meta">
<span>
OBJID <b>{menuId}</b>
</span>
<span>
<b>{menu.regdate ?? menu.REGDATE ?? "—"}</b>
</span>
<span>
<b>{menu.writer ?? menu.WRITER ?? "—"}</b>
</span>
</div>
</div>
<div className="v5-mm-sv-grid">
<div className="v5-mm-sv-side">
<h4> </h4>
<p> , , .</p>
</div>
<div className="v5-mm-sv-fields">
<div className="v5-mm-sv-row">
<label>
<span className="req">*</span>
</label>
<input
className="v5-mm-inp"
value={form.menuNameKor}
onChange={(e) => set("menuNameKor", e.target.value)}
/>
</div>
<div className="v5-mm-sv-row-2">
<div className="v5-mm-sv-row">
<label> </label>
<select
className="v5-mm-inp"
value={form.langKey}
onChange={(e) => set("langKey", e.target.value)}
>
<option value="">( )</option>
{langKeys.map((k) => (
<option key={k.keyId} value={k.langKey}>
{k.langKey}
{k.description ? `${k.description}` : ""}
</option>
))}
</select>
</div>
<div className="v5-mm-sv-row">
<label></label>
<div className="v5-mm-iconpicker-slot">
<MenuIconPicker
value={form.menuIcon}
onChange={(iconName) => set("menuIcon", iconName)}
label=""
/>
</div>
</div>
</div>
<div className="v5-mm-sv-row">
<label></label>
<textarea
className="v5-mm-inp"
value={form.menuDesc}
onChange={(e) => set("menuDesc", e.target.value)}
/>
</div>
</div>
</div>
<div className="v5-mm-sv-grid">
<div className="v5-mm-sv-side">
<h4></h4>
<p> URL . .</p>
</div>
<div className="v5-mm-sv-fields">
<div className="v5-mm-sv-row">
<label>URL</label>
<input
className="v5-mm-inp"
value={form.menuUrl}
onChange={(e) => set("menuUrl", e.target.value)}
placeholder="/admin/..."
/>
<div className="help">//POP .</div>
</div>
</div>
</div>
<div className="v5-mm-sv-grid">
<div className="v5-mm-sv-side">
<h4> &amp; </h4>
<p> / .</p>
</div>
<div className="v5-mm-sv-fields">
<div className="v5-mm-sv-row-2">
<div className="v5-mm-sv-row">
<label></label>
<select
className="v5-mm-inp"
value={form.companyCode || "*"}
onChange={(e) => set("companyCode", e.target.value)}
>
<option value="*"> (*)</option>
{companies.map((c) => (
<option key={c.company_code} value={c.company_code}>
{c.company_name}
{c.company_code ? ` (${c.company_code})` : ""}
</option>
))}
</select>
</div>
<div className="v5-mm-sv-row">
<label></label>
<input className="v5-mm-inp" value={parentName || "최상위"} readOnly />
</div>
</div>
<div className="v5-mm-sv-row-2">
<div className="v5-mm-sv-row">
<label></label>
<input
className="v5-mm-inp"
type="number"
value={form.seq}
onChange={(e) => set("seq", Number(e.target.value) || 0)}
/>
</div>
<div className="v5-mm-sv-row">
<label></label>
<label
className={`v5-mm-tg${statusOn ? " on" : ""}`}
onClick={() => set("status", statusOn ? "inactive" : "active")}
>
<span className="sw" />
<span className="lbl">{statusOn ? "활성" : "비활성"}</span>
</label>
</div>
</div>
</div>
</div>
<div className="v5-mm-sv-ft">
<div style={{ display: "flex", gap: ".4rem" }}>
<button className="v5-mm-btn danger sm" onClick={() => onDelete(menuId)}>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M3 6h18M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2m3 0v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6" />
</svg>
</button>
<button className="v5-mm-btn sm" onClick={() => onOpenAdvanced(menuId)}>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M12 20h9M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z" />
</svg>
</button>
</div>
<div style={{ display: "flex", gap: ".4rem", alignItems: "center" }}>
{isDirty && (
<span className="v5-mm-unsaved">
<span className="d" />
</span>
)}
<button
className="v5-mm-btn"
disabled={!isDirty || saving}
onClick={() => setForm(initial)}
>
</button>
<button className="v5-mm-btn primary" disabled={!isDirty || saving} onClick={handleSave}>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
<path d="M5 12l5 5L20 7" />
</svg>
{saving ? "저장 중…" : "저장"}
</button>
</div>
</div>
</div>
);
}