chore(admin): 공통코드 관리 메뉴/UI/API 일괄 제거
Deploy momo-erp / deploy (push) Successful in 2m52s

momo 영역에서 미사용. FITO 레거시 잔존 코드 정리:
- sidebar.tsx: __sys_code 가상 메뉴 항목 제거
- admin-panel/page.tsx: code 탭/메뉴/CodeManagement 함수 삭제
- admin-panel/code-form/: 폼 페이지 디렉토리 통째로 삭제
- api/admin/codes/: list/detail/save 라우트 통째로 삭제

/api/common/code-list (조회 전용) 는 product/part-change 등이
드롭다운 로드용으로 쓰고 있어 보존.
This commit is contained in:
chpark
2026-05-13 16:29:48 +09:00
parent 5778b845d1
commit 29852110dc
6 changed files with 3 additions and 502 deletions
-200
View File
@@ -1,200 +0,0 @@
"use client";
import { useState, useEffect, Suspense } from "react";
import { useSearchParams } from "next/navigation";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import Swal from "sweetalert2";
interface ParentCode { CODE_ID: string; CODE_NAME: string }
function CodeForm() {
const searchParams = useSearchParams();
const codeId = searchParams.get("codeId");
const isNew = !codeId;
const [form, setForm] = useState<Record<string, string>>({});
const [parents, setParents] = useState<ParentCode[]>([]);
const [loading, setLoading] = useState(false);
const [parentOpen, setParentOpen] = useState(false);
const [parentSearch, setParentSearch] = useState("");
const set = (k: string, v: string) => setForm((p) => ({ ...p, [k]: v }));
const selectedParent = parents.find((p) => p.CODE_ID === (form.parent_code_id || ""));
const filteredParents = parentSearch
? parents.filter(
(p) =>
p.CODE_NAME.toLowerCase().includes(parentSearch.toLowerCase()) ||
p.CODE_ID.toLowerCase().includes(parentSearch.toLowerCase())
)
: parents;
// 분류(최상위) 코드 로드
useEffect(() => {
fetch("/api/admin/codes", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ parentOnly: true }),
})
.then((r) => r.json())
.then((d) => {
const rows = (d.RESULTLIST || []) as Array<Record<string, unknown>>;
setParents(rows.map((r) => ({ CODE_ID: String(r.CODE_ID || ""), CODE_NAME: String(r.CODE_NAME || "") })));
})
.catch(() => {});
}, []);
// 수정 시 상세 로드
useEffect(() => {
if (codeId) {
fetch("/api/admin/codes/detail", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ codeId }),
})
.then((r) => r.json())
.then((d) => {
if (d.success) setForm(d.data);
})
.catch(() => {});
}
}, [codeId]);
const handleSave = async () => {
if (!form.code_name) {
Swal.fire("알림", "코드명을 입력하세요.", "warning");
return;
}
setLoading(true);
try {
const res = await fetch("/api/admin/codes/save", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ ...form, actionType: isNew ? "regist" : "update" }),
});
const data = await res.json();
if (data.success) {
await Swal.fire({ icon: "success", title: data.message || "저장되었습니다.", timer: 1500, showConfirmButton: false });
if (window.opener) {
try { window.opener.location.reload(); } catch {}
}
if (isNew) window.close();
} else {
Swal.fire("오류", data.message || "저장 실패", "error");
}
} catch {
Swal.fire("오류", "서버 오류", "error");
} finally {
setLoading(false);
}
};
return (
<div className="p-5 bg-gray-50 min-h-screen">
<h2 className="text-base font-bold text-gray-800 mb-4 flex items-center gap-1">
<span className="text-blue-500"></span>
</h2>
<div className="bg-white border rounded p-4">
<table className="w-full border-collapse text-sm">
<tbody>
<tr>
<th className="bg-[#5c6d97] text-white font-medium text-center py-2.5 w-[130px] border border-white"></th>
<td className="border border-gray-200 p-1.5">
<div className="relative">
<input
type="text"
value={parentOpen ? parentSearch : (selectedParent?.CODE_NAME || "")}
placeholder="선택 (검색 가능)"
onFocus={() => { setParentOpen(true); setParentSearch(""); }}
onBlur={() => setTimeout(() => setParentOpen(false), 150)}
onChange={(e) => setParentSearch(e.target.value)}
className="h-9 w-full rounded border border-gray-300 bg-white pl-2 pr-8 text-sm outline-none focus:ring-1 focus:ring-blue-400"
/>
{selectedParent && !parentOpen && (
<button
type="button"
onClick={() => { set("parent_code_id", ""); setParentSearch(""); }}
className="absolute right-2 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-700 text-sm leading-none"
aria-label="선택 해제"
>
×
</button>
)}
{parentOpen && (
<div className="absolute z-50 left-0 right-0 top-full mt-1 max-h-60 overflow-y-auto bg-white border border-gray-300 rounded shadow-lg">
<button
type="button"
onMouseDown={(e) => {
e.preventDefault();
set("parent_code_id", "");
setParentSearch("");
setParentOpen(false);
}}
className="w-full text-left px-2 py-1.5 text-sm text-gray-500 italic hover:bg-gray-50 border-b"
>
( )
</button>
{filteredParents.length === 0 ? (
<div className="p-2 text-xs text-gray-400 text-center"> </div>
) : (
filteredParents.map((p) => (
<button
key={p.CODE_ID}
type="button"
onMouseDown={(e) => {
e.preventDefault();
set("parent_code_id", p.CODE_ID);
setParentSearch("");
setParentOpen(false);
}}
className="w-full text-left px-2 py-1.5 text-sm hover:bg-blue-50 flex items-center gap-2"
>
<span className="text-[11px] text-gray-400 font-mono shrink-0 w-[70px]">{p.CODE_ID}</span>
<span className="truncate">{p.CODE_NAME}</span>
</button>
))
)}
</div>
)}
</div>
</td>
</tr>
<tr>
<th className="bg-[#5c6d97] text-white font-medium text-center py-2.5 border border-white">
<span className="text-red-300">*</span>
</th>
<td className="border border-gray-200 p-1.5">
<Input value={form.code_name || ""} onChange={(e) => set("code_name", e.target.value)} />
</td>
</tr>
<tr>
<th className="bg-[#5c6d97] text-white font-medium text-center py-2.5 border border-white"></th>
<td className="border border-gray-200 p-1.5">
<select
value={form.status || ""}
onChange={(e) => set("status", e.target.value)}
className="h-9 w-full rounded border border-gray-300 bg-white px-2 text-sm"
>
<option value=""></option>
<option value="active"></option>
<option value="inActive"></option>
</select>
</td>
</tr>
</tbody>
</table>
</div>
<div className="flex justify-center gap-2 mt-5">
<Button onClick={handleSave} disabled={loading}>{loading ? "저장 중..." : "저장"}</Button>
<Button variant="secondary" onClick={() => window.close()}></Button>
</div>
</div>
);
}
export default function Page() {
return (
<Suspense fallback={<div className="p-8 text-center text-gray-400"> ...</div>}>
<CodeForm />
</Suspense>
);
}
+3 -189
View File
@@ -14,7 +14,7 @@ import {
} from "lucide-react";
// admin/adminMainFS.do 대응 - 관리자 팝업 페이지
type AdminTab = "menu" | "auth" | "user" | "dept" | "code" | "supply" | "template" | "exchange" | "log-file" | "log-login" | "log-mail" | "ref-customer" | "ref-material" | "ref-car" | "ref-car-grade" | "ref-product-group" | "ref-product" | "spec-data-category" | "car-option";
type AdminTab = "menu" | "auth" | "user" | "dept" | "supply" | "template" | "exchange" | "log-file" | "log-login" | "log-mail" | "ref-customer" | "ref-material" | "ref-car" | "ref-car-grade" | "ref-product-group" | "ref-product" | "spec-data-category" | "car-option";
const ADMIN_MENUS = [
{
@@ -32,7 +32,6 @@ const ADMIN_MENUS = [
{
label: "기준정보관리", icon: Database,
items: [
{ key: "code" as AdminTab, label: "공통코드관리" },
{ key: "supply" as AdminTab, label: "공급업체관리" },
{ key: "template" as AdminTab, label: "템플릿 관리" },
{ key: "exchange" as AdminTab, label: "환율관리" },
@@ -67,7 +66,6 @@ const LABEL_TO_TAB: Record<string, AdminTab> = {
"권한 관리": "auth",
"부서 관리": "dept",
"사용자 관리": "user",
"공통코드관리": "code",
"공급업체관리": "supply",
"템플릿 관리": "template",
"환율관리": "exchange",
@@ -99,7 +97,7 @@ interface SidebarGroup {
}
const VALID_TABS: AdminTab[] = [
"menu","auth","user","dept","code","supply","template","exchange",
"menu","auth","user","dept","supply","template","exchange",
"log-file","log-login","log-mail",
"ref-customer","ref-material","ref-car","ref-car-grade",
"ref-product-group","ref-product","spec-data-category","car-option",
@@ -230,7 +228,6 @@ export default function AdminPanelPage() {
{/* 우측 콘텐츠 */}
<main className="flex-1 overflow-auto p-4">
{activeTab === "user" && <UserManagement />}
{activeTab === "code" && <CodeManagement />}
{activeTab === "menu" && <MenuManagement />}
{activeTab === "auth" && <AuthManagement />}
{activeTab === "dept" && <DeptManagement />}
@@ -248,7 +245,7 @@ export default function AdminPanelPage() {
{activeTab === "spec-data-category" && <SpecDataCategoryManagement />}
{activeTab === "car-option" && <CarOptionManagement />}
{/* 기타 탭은 공통 Placeholder (DB 테이블 없음) */}
{!["user","code","menu","auth","dept","supply","log-login","log-file","log-mail","template","exchange","ref-customer","ref-material","ref-car","ref-product-group","ref-product","spec-data-category","car-option"].includes(activeTab) && (
{!["user","menu","auth","dept","supply","log-login","log-file","log-mail","template","exchange","ref-customer","ref-material","ref-car","ref-product-group","ref-product","spec-data-category","car-option"].includes(activeTab) && (
<PlaceholderContent title={groups.flatMap(g => g.items).find(i => LABEL_TO_TAB[i.label] === activeTab)?.label || activeTab} />
)}
</main>
@@ -352,189 +349,6 @@ function UserManagement() {
);
}
// ==========================================
// 공통코드 관리 (admin/codeCategory/codeCategoryMngList.jsp)
// ==========================================
interface CodeRow {
OBJID: string;
CODE_ID: string;
CODE_NAME: string;
EXT_VAL: string;
PARENT_CODE_ID: string;
WRITER_NAME: string;
REGDATE: string;
STATUS: string;
}
function CodeManagement() {
const [searchCode, setSearchCode] = useState("");
const [searchStatus, setSearchStatus] = useState("active");
const [data, setData] = useState<CodeRow[]>([]);
const [expanded, setExpanded] = useState<Set<string>>(new Set());
const fetchData = useCallback(async () => {
const res = await fetch("/api/admin/codes", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ code_name: searchCode, status: searchStatus }),
});
if (res.ok) {
const json = await res.json();
const rows = (json.RESULTLIST || []) as Record<string, unknown>[];
const mapped: CodeRow[] = rows.map((r) => ({
OBJID: String(r.OBJID || ""),
CODE_ID: String(r.CODE_ID || ""),
CODE_NAME: String(r.CODE_NAME || ""),
EXT_VAL: String(r.EXT_VAL || ""),
PARENT_CODE_ID: String(r.PARENT_CODE_ID || ""),
WRITER_NAME: String(r.WRITER_NAME || ""),
REGDATE: String(r.REGDATE || ""),
STATUS: String(r.STATUS || ""),
}));
setData(mapped);
}
}, [searchCode, searchStatus]);
useEffect(() => { fetchData(); }, [fetchData]);
// 검색어가 있으면 평면 리스트, 없으면 트리 모드
const isSearching = searchCode.trim().length > 0;
// code_id → 자식 리스트 매핑
const childrenMap = useMemo(() => {
const m = new Map<string, CodeRow[]>();
data.forEach((r) => {
const key = r.PARENT_CODE_ID || "";
if (!m.has(key)) m.set(key, []);
m.get(key)!.push(r);
});
return m;
}, [data]);
// 표시 행 (트리 평탄화 또는 검색 평면)
const visibleRows = useMemo(() => {
if (isSearching) {
return data.map((r) => ({ row: r, depth: 0, hasChildren: false }));
}
const out: { row: CodeRow; depth: number; hasChildren: boolean }[] = [];
const walk = (parentId: string, depth: number) => {
const kids = childrenMap.get(parentId) || [];
kids.forEach((k) => {
const hasChildren = (childrenMap.get(k.CODE_ID) || []).length > 0;
out.push({ row: k, depth, hasChildren });
if (expanded.has(k.CODE_ID)) walk(k.CODE_ID, depth + 1);
});
};
walk("", 0);
return out;
}, [data, childrenMap, expanded, isSearching]);
const toggleExpand = (codeId: string) => {
setExpanded((prev) => {
const next = new Set(prev);
if (next.has(codeId)) next.delete(codeId); else next.add(codeId);
return next;
});
};
const expandAll = () => setExpanded(new Set(data.map((r) => r.CODE_ID)));
const collapseAll = () => setExpanded(new Set());
const openCodeDetail = (codeId: string) => {
window.open(`/admin-panel/code-form?codeId=${codeId}`, "codeFormPopup", "width=500,height=600");
};
return (
<div>
<h2 className="text-lg font-bold text-gray-800 mb-4"></h2>
<SearchForm onSearch={fetchData}>
<SearchField label="코드명">
<Input value={searchCode} onChange={(e) => setSearchCode(e.target.value)} className="w-[150px]" />
</SearchField>
<SearchField label="상태">
<select value={searchStatus} onChange={(e) => setSearchStatus(e.target.value)}
className="h-9 w-[120px] rounded border border-gray-300 bg-white px-3 text-sm">
<option value="active"></option>
<option value="inActive"></option>
<option value=""></option>
</select>
</SearchField>
</SearchForm>
<div className="flex justify-end gap-2 mb-3">
{!isSearching && (
<>
<Button size="sm" variant="secondary" onClick={expandAll}> </Button>
<Button size="sm" variant="secondary" onClick={collapseAll}> </Button>
</>
)}
<Button size="sm" onClick={() => window.open("/admin-panel/code-form", "codeFormPopup", "width=500,height=600")}></Button>
<Button size="sm" variant="secondary" onClick={fetchData}></Button>
</div>
<div className="text-xs text-gray-500 mb-2">
{data.length}{isSearching ? " (검색 결과 - 평면 리스트)" : ` · 트리 모드 (최상위 ${(childrenMap.get("") || []).length}개)`}
</div>
<div className="border border-gray-200 rounded bg-white overflow-auto" style={{ maxHeight: "calc(100vh - 310px)" }}>
<table className="w-full text-sm">
<thead className="sticky top-0 bg-gray-100 z-10">
<tr className="border-b">
<th className="p-2 text-left w-[140px] font-medium text-gray-700">ID</th>
<th className="p-2 text-left font-medium text-gray-700"></th>
<th className="p-2 text-left w-[120px] font-medium text-gray-700">CODE NO</th>
<th className="p-2 text-center w-[100px] font-medium text-gray-700"></th>
<th className="p-2 text-center w-[110px] font-medium text-gray-700"></th>
<th className="p-2 text-center w-[80px] font-medium text-gray-700"></th>
</tr>
</thead>
<tbody>
{visibleRows.map(({ row, depth, hasChildren }) => {
const isExp = expanded.has(row.CODE_ID);
return (
<tr
key={row.OBJID}
className="border-t hover:bg-gray-50 cursor-pointer"
onClick={() => openCodeDetail(row.CODE_ID)}
>
<td className="p-1.5 font-mono text-xs text-blue-600">{row.CODE_ID}</td>
<td className="p-1.5">
<div className="flex items-center" style={{ paddingLeft: `${depth * 18}px` }}>
{!isSearching && hasChildren ? (
<button
onClick={(e) => { e.stopPropagation(); toggleExpand(row.CODE_ID); }}
className="w-5 h-5 flex items-center justify-center mr-1 text-gray-400 hover:text-gray-700 text-[10px]"
>
{isExp ? "▼" : "▶"}
</button>
) : (
<span className="w-5 h-5 mr-1 inline-flex items-center justify-center text-gray-300">·</span>
)}
<span className={cn(depth === 0 && !isSearching ? "font-bold text-gray-900" : "text-gray-700")}>
{row.CODE_NAME}
</span>
</div>
</td>
<td className="p-1.5 text-gray-600">{row.EXT_VAL || "-"}</td>
<td className="p-1.5 text-center text-gray-600">{row.WRITER_NAME || "-"}</td>
<td className="p-1.5 text-center text-gray-600">{row.REGDATE}</td>
<td className="p-1.5 text-center">
{row.STATUS === "active" ? (
<span className="text-xs text-green-600"></span>
) : (
<span className="text-xs text-red-500"></span>
)}
</td>
</tr>
);
})}
{visibleRows.length === 0 && (
<tr><td colSpan={6} className="p-8 text-center text-gray-400"> .</td></tr>
)}
</tbody>
</table>
</div>
</div>
);
}
// ==========================================
// 메뉴 관리 (트리구조 CRUD)
// ==========================================
-23
View File
@@ -1,23 +0,0 @@
import { NextRequest, NextResponse } from "next/server";
import { pool } from "@/lib/db";
import { getSession } from "@/lib/session";
export async function POST(request: NextRequest) {
const user = await getSession();
if (!user) return NextResponse.json({ success: false }, { status: 401 });
const body = await request.json();
const client = await pool.connect();
try {
const result = await client.query(
`SELECT objid::text AS "objid", code_id AS "code_id", code_name AS "code_name",
ext_val AS "ext_val", parent_code_id AS "parent_code_id",
COALESCE(status, 'active') AS "status"
FROM comm_code WHERE code_id = $1`,
[body.codeId || ""]
);
return NextResponse.json({ success: true, data: result.rows[0] || null });
} catch (e) {
console.error("Code detail:", e);
return NextResponse.json({ success: false, message: "조회 중 오류가 발생했습니다." });
} finally { client.release(); }
}
-50
View File
@@ -1,50 +0,0 @@
import { NextRequest, NextResponse } from "next/server";
import { queryRows } from "@/lib/db";
import { getSession } from "@/lib/session";
// 공통코드관리 목록 조회
// body.parentOnly === true → 최상위 코드만 (code-form 분류선택 드롭다운용)
// 그 외 → 전체 코드 (트리 렌더링용)
export async function POST(request: NextRequest) {
const user = await getSession();
if (!user) return NextResponse.json({ success: false }, { status: 401 });
const body = await request.json();
const conditions: string[] = [];
const params: unknown[] = [];
let idx = 1;
if (body.parentOnly === true) {
conditions.push("(CC.parent_code_id IS NULL OR CC.parent_code_id = '')");
}
if (body.code_name) {
conditions.push(`CC.code_name LIKE '%' || $${idx++} || '%'`);
params.push(body.code_name);
}
if (body.status) {
conditions.push(`COALESCE(CC.status, 'active') = $${idx++}`);
params.push(body.status);
}
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
const sql = `
SELECT CC.objid::text AS "OBJID",
CC.code_id AS "CODE_ID",
CC.code_name AS "CODE_NAME",
CC.ext_val AS "EXT_VAL",
COALESCE(CC.parent_code_id, '') AS "PARENT_CODE_ID",
COALESCE((SELECT user_name FROM user_info WHERE user_id = CC.writer LIMIT 1), CC.writer) AS "WRITER_NAME",
TO_CHAR(CC.regdate, 'YYYY-MM-DD') AS "REGDATE",
COALESCE(CC.status, 'active') AS "STATUS"
FROM comm_code CC
${where}
ORDER BY
COALESCE(CC.parent_code_id, '') ASC,
CC.code_id ASC
`;
const rows = await queryRows(sql, params);
return NextResponse.json({ RESULTLIST: rows, TOTAL_CNT: rows.length });
}
-39
View File
@@ -1,39 +0,0 @@
import { NextRequest, NextResponse } from "next/server";
import { pool } from "@/lib/db";
import { getSession } from "@/lib/session";
export async function POST(request: NextRequest) {
const user = await getSession();
if (!user) return NextResponse.json({ success: false }, { status: 401 });
const body = await request.json();
const isNew = body.actionType === "regist" || !body.objid;
const objId = isNew ? Date.now() : Number(body.objid);
const status = body.status || "active";
const client = await pool.connect();
try {
await client.query(
`INSERT INTO comm_code (objid, code_id, code_name, ext_val, parent_code_id, writer, regdate, status)
VALUES ($1::numeric, $2, $3, $4, $5, $6, now(), $7)
ON CONFLICT (objid) DO UPDATE SET
code_name=EXCLUDED.code_name,
ext_val=EXCLUDED.ext_val,
parent_code_id=EXCLUDED.parent_code_id,
status=EXCLUDED.status`,
[
objId,
body.code_id || "",
body.code_name || "",
body.ext_val || "",
body.parent_code_id || "",
user.userId,
status,
]
);
return NextResponse.json({ success: true, message: isNew ? "등록되었습니다." : "수정되었습니다." });
} catch (e) {
console.error("Code save:", e);
return NextResponse.json({ success: false, message: "저장 중 오류가 발생했습니다." });
} finally {
client.release();
}
}
-1
View File
@@ -47,7 +47,6 @@ const ADMIN_SYSTEM_MENU: MenuItem = {
{ objid: "__sys_auth", menuNameKor: "권한 관리", menuNameEng: "Auth", menuUrl: "/admin-panel?tab=auth", parentObjId: "__sys__", menuOrder: "2", level: "2" },
{ objid: "__sys_dept", menuNameKor: "부서 관리", menuNameEng: "Dept", menuUrl: "/admin-panel?tab=dept", parentObjId: "__sys__", menuOrder: "3", level: "2" },
{ objid: "__sys_menu", menuNameKor: "메뉴 관리", menuNameEng: "Menu", menuUrl: "/admin-panel?tab=menu", parentObjId: "__sys__", menuOrder: "4", level: "2" },
{ objid: "__sys_code", menuNameKor: "공통코드 관리", menuNameEng: "Code", menuUrl: "/admin-panel?tab=code", parentObjId: "__sys__", menuOrder: "5", level: "2" },
{ objid: "__sys_supply", menuNameKor: "공급업체 관리", menuNameEng: "Vendors", menuUrl: "/admin-panel?tab=supply", parentObjId: "__sys__", menuOrder: "6", level: "2" },
{ objid: "__sys_login", menuNameKor: "로그인 로그", menuNameEng: "Login Log", menuUrl: "/admin-panel?tab=log-login", parentObjId: "__sys__", menuOrder: "7", level: "2" },
{ objid: "__sys_file", menuNameKor: "파일 로그", menuNameEng: "File Log", menuUrl: "/admin-panel?tab=log-file", parentObjId: "__sys__", menuOrder: "8", level: "2" },