스마트서비스 최종완료보고 버전

This commit is contained in:
CodeTempla
2026-05-19 20:58:19 +09:00
commit d26eb50875
405 changed files with 46755 additions and 0 deletions
@@ -0,0 +1,125 @@
"use client";
import React, { useState } from "react";
import Label from '@/components/form/Label2';
import Input from '@/components/form/input/InputField2';
import CtModal from "@/components/CtModal";
import { useMultiState } from "@/hooks/useMultiState";
import api, { formatBusinessNumber } from '@/lib/_AG';
export interface BizGroupModalProps {
isModify: boolean; // external set
pid: string; // external set
//
name: string;
biz_reg_num: string;
phone: string;
email: string;
ceo: string;
address: string;
}
interface AddModifyBizGroupModalProps {
multiState: ReturnType<typeof useMultiState<BizGroupModalProps>>;
onOk: (insertedId: number) => void;
isOpen: boolean;
closeModal: () => void;
}
export default function AddModifyBizGroupModal({ multiState, onOk, isOpen, closeModal }: AddModifyBizGroupModalProps) {
const handleClickAddOrModify = () => {
if (multiState.values.name == "") {
alert("그룹명(사업자명)을 입력하셔야 합니다.");
return;
}
//else if (multiState.values.biz_reg_num == "") {
// alert("사업자등록번호를 입력하셔야 합니다.");
// return;
//}
if (multiState.values.isModify) {
api.post('/api/modify-biz-group.do', multiState.values).then((resp) => {
console.log(resp);
if (resp.data.errCode == 0) {
alert("수정되었습니다.");
onOk(-1);
}
else if (resp.data.errCode == -11) {
alert("이미 존재하는 사업자번호입니다.");
}
else if (resp.data.errCode == -17) {
alert("잘못된 형식의 사업자번호입니다.");
}
else {
alert("일시적으로 사용할 수 없습니다 : " + resp.data.errCode);
}
})
.catch((err) => console.error(err))
}
else {
api.post('/api/add-biz-group.do', multiState.values).then((resp) => {
console.log(resp);
if (resp.data.errCode == 0) {
alert("추가되었습니다.");
onOk(resp.data.result.biz_group_id);
}
else if (resp.data.errCode == -11) {
alert("이미 존재하는 사업자번호입니다.");
}
else if (resp.data.errCode == -17) {
alert("잘못된 형식의 사업자번호입니다.");
}
else {
alert("일시적으로 사용할 수 없습니다 : " + resp.data.errCode);
}
})
.catch((err) => console.error(err))
}
}
const handleChangeBizRegNum = (value: string) => {
multiState.set("biz_reg_num", formatBusinessNumber(value));
}
return (
// <div className="rounded-2xl border border-gray-200 bg-white dark:border-gray-800 dark:bg-white/[0.03]">
<CtModal
title={multiState.values.isModify ? "사업자정보 수정": "사업자 등록"}
description={multiState.values.isModify ? "수정할 사업자원장 정보를 입력하세요." : "새로 등록할 사업자원장 정보를 입력하세요."}
okBtnText={multiState.values.isModify ? "수정": "추가"}
onClickOk={handleClickAddOrModify}
isOpen={isOpen}
closeModal={closeModal}
className="max-w-[700px] p-6 lg:p-10"
>
<div>
<Label redPoint>()</Label>
<Input type="text" value={multiState.values.name} onChange={(e) => multiState.set("name", e.target.value)} />
</div>
<div>
<Label></Label>
<Input type="text" value={multiState.values.biz_reg_num} onChange={(e) => handleChangeBizRegNum(e.target.value)} />
</div>
<div>
<Label></Label>
<Input type="text" value={multiState.values.phone} onChange={(e) => multiState.set("phone", e.target.value)} />
</div>
<div>
<Label></Label>
<Input type="text" value={multiState.values.email} onChange={(e) => multiState.set("email", e.target.value)} />
</div>
<div>
<Label></Label>
<Input type="text" value={multiState.values.ceo} onChange={(e) => multiState.set("ceo", e.target.value)} />
</div>
<div>
<Label></Label>
<Input type="text" value={multiState.values.address} onChange={(e) => multiState.set("address", e.target.value)} />
</div>
</CtModal>
// </div>
);
};
@@ -0,0 +1,15 @@
"use client"
import PageBreadcrumb from "@/components/common/PageBreadCrumb2";
import React from "react";
import { DocsIcon, FileIcon, PlusIcon, TrashBinIcon, TableIcon, PencilIcon } from "@/icons";
import BizGroupContent from "./BizGroupContent";
export default function BizGroup() {
return (
<div>
<PageBreadcrumb pageTitle1="계정 및 그룹관리" pageTitle2="사업자 그룹 관리" />
<BizGroupContent title="사업자원장 목록 조회" titleIcon={<TableIcon />} />
</div>
);
}
@@ -0,0 +1,437 @@
"use client"
import PageBreadcrumb from "@/components/common/PageBreadCrumb2";
import ComponentCard from '@/components/common/ComponentCard2';
import Button from '@/components/ui/button/Button2';
import { DocsIcon, FileIcon, PlusIcon, TrashBinIcon, TableIcon, PencilIcon } from "@/icons";
import Label from '@/components/form/Label';
import WithLabel from '@/components/form/WithLabel';
import Input from '@/components/form/input/InputField2';
import DatePicker from '@/components/form/date-picker2';
import React, { Key } from "react";
import { useEffect, useState } from 'react';
import api, { convertDateTime3, formatBusinessNumber, useAuthStore } from '@/lib/_AG';
import { useModal } from "@/hooks/useModal";
import AddModifyBizGroupModal, { BizGroupModalProps } from './AddModifyBizGroupModal';
import { useMultiState } from "@/hooks/useMultiState";
import CtTree, { CtTreeNode, expandAndSelectTreeNode, getAllNodeKeys, findParentKeys, findNodeByKey, addNodeByKey, changeNodeByKey, deleteNodeByKey } from "@/components/CtTree";
import { tree } from "next/dist/build/templates/app-page";
import { useRouter } from "next/router";
const SEARCH_LABEL_WIDTH = 24;
// api component
//
export function reqBizGroupTree2(
isRoot: boolean,
pid: string,
top_group_id: string,
date_start: string,
date_end: string,
name: string,
biz_reg_num: string,
treeData: CtTreeNode[],
setTreeData: React.Dispatch<React.SetStateAction<CtTreeNode[]>>,
onResponse?: (resp: any) => void
) {
const params: any = {
isRoot: isRoot,
pid: pid,
top_group_id: top_group_id,
date_start: date_start,
date_end: date_end,
}
if (isRoot == true) {
params.name = name;
params.biz_reg_num = biz_reg_num;
}
api.post('/api/get-biz-group2.do', params).then((resp) => {
console.log(resp);
if (resp.data.errCode == 0) {
if (resp.data.result.tree.length == 0) {
if (isRoot == true) {
alert("조회 결과가 없습니다.");
setTreeData([]);
}
return;
}
if (pid == "") {
setTreeData(resp.data.result.tree);
}
else {
setTreeData(treeData => addNodeByKey(treeData, pid, resp.data.result.tree));
}
}
else {
alert("일시적으로 사용할 수 없습니다 : " + resp.data.errCode);
}
onResponse?.(resp);
})
.catch((err) => console.error(err))
}
export async function reqBizGroupTree3(
biz_group_id: string,
setTreeData: React.Dispatch<React.SetStateAction<CtTreeNode[]>>,
setSelectedTreeKeys: React.Dispatch<React.SetStateAction<Key[]>>,
setExpandedTreeKeys: React.Dispatch<React.SetStateAction<Key[]>>,
onSuccess?: (resp: any) => void
) {
try {
const params = { biz_group_id };
const resp = await api.post('/api/get-biz-group3.do', params);
//console.log(resp);
if (resp.data.errCode === 0) {
if (resp.data.result.tree.length === 0) {
setTreeData([]);
return;
}
setTreeData(resp.data.result.tree);
setSelectedTreeKeys([resp.data.result.tree[0].key]);
const keys = getAllNodeKeys(resp.data.result.tree);
setExpandedTreeKeys(keys);
} else {
alert("일시적으로 사용할 수 없습니다 : " + resp.data.errCode);
}
if (onSuccess) onSuccess(resp);
} catch (err) {
console.error(err);
}
}
//
interface BizGroupContentProps {
title?: string;
titleIcon?: React.ReactNode;
preloadTreeData?: CtTreeNode[];
}
export default function BizGroupContent({
title,
titleIcon,
preloadTreeData
}: BizGroupContentProps) {
const tokenPayload = useAuthStore((s) => s.tokenPayload); //Bruce, JWT
const {isOpen, openModal, closeModal} = useModal();
// tree part
const [treeData, setTreeData] = useState<CtTreeNode[]>(preloadTreeData ? preloadTreeData : []);
const [selectedTreeKeys, setSelectedTreeKeys] = useState<Key[]>(preloadTreeData ? [preloadTreeData[0].key as string] : []);
const [expandedTreeKeys, setExpandedTreeKeys] = useState<Key[]>(preloadTreeData ? getAllNodeKeys(preloadTreeData) : []);
// search fields
const [name, setName] = useState<string>("");
const [biz_reg_num, setBizRegNum] = useState<string>("");
const [date_start, setDateStart] = useState<string>("");
const [date_end, setDateEnd] = useState<string>("");
//
// form fields
const [formInfo, setFormInfo] = useState({name: "", biz_reg_num: "", email: "", phone: "", ceo: "", address: ""});
const handleChangeFormInfo = (field: string, value?: string | number) => { setFormInfo(prev => ({ ...prev, [field]: value ?? "" })); };
// modal values
const modalData = useMultiState<BizGroupModalProps>({isModify: false, pid: "", name: "", biz_reg_num: "", phone: "", email: "", ceo: "", address: "" });
// function part
//
function reqRemoveBizGroup(biz_group_id: Key) {
const params = {
ids: biz_group_id
}
api.post('/api/remove-biz-group.do', params).then((resp) => {
console.log(resp);
if (resp.data.errCode == 0) {
setTreeData(treeData => deleteNodeByKey(treeData, biz_group_id));
setSelectedTreeKeys([]);
setFormInfo({name: "", biz_reg_num: "", email: "", phone: "", ceo: "", address: ""});
alert("삭제되었습니다.");
}
else {
alert("일시적으로 사용할 수 없습니다 : " + resp.data.errCode);
}
})
.catch((err) => console.error(err))
}
function reqModifyBizGroup(biz_group_id: Key) {
if (formInfo.name == "") {
alert("그룹명(사업자명)을 입력하셔야 합니다.");
return;
}
//else if (formInfo.biz_reg_num == "") {
// alert("사업자등록번호를 입력하셔야 합니다.");
// return;
//}
const params = {
biz_group_id: biz_group_id,
name: formInfo.name,
biz_reg_num: formInfo.biz_reg_num,
email: formInfo.email,
phone: formInfo.phone,
ceo: formInfo.ceo,
address: formInfo.address,
}
api.post('/api/modify-biz-group.do', params).then((resp) => {
console.log(resp);
if (resp.data.errCode == 0) {
const node = findNodeByKey(treeData, biz_group_id);
node.key = biz_group_id;
//... node.title = formInfo.name + "(" + formInfo.biz_reg_num + ")";
node.title = formInfo.name;
node.name = formInfo.name;
node.biz_reg_num = formInfo.biz_reg_num;
node.email = formInfo.email;
node.phone = formInfo.phone;
node.ceo = formInfo.ceo;
node.address = formInfo.address;
setTreeData(treeData => changeNodeByKey(treeData, biz_group_id, node));
alert("수정되었습니다.");
}
else if (resp.data.errCode == -11) {
alert("이미 존재하는 사업자번호입니다.");
}
else if (resp.data.errCode == -17) {
alert("잘못된 형식의 사업자번호입니다.");
}
else {
alert("일시적으로 사용할 수 없습니다 : " + resp.data.errCode);
}
})
.catch((err) => console.error(err))
}
//
// event processing part
//
const handleChangeBizRegNum = (value: string) => {
setBizRegNum(formatBusinessNumber(value));
}
const handleClickSearch = () => {
if (tokenPayload?.permission == 1) {
if (name == "" && biz_reg_num == "") {
alert("그룹명(사업자명) 또는 사업자등록번호를 입력하셔야 합니다.");
return;
}
if (name != "" && name.length < 3) {
alert("그룹명(사업자명)은 세글자 이상 입력해야 합니다.");
return;
}
if (biz_reg_num != "" && biz_reg_num.length < 3) {
alert("사업자등록번호는 세글자 이상 입력해야 합니다.");
return;
}
setSelectedTreeKeys(preloadTreeData ? [preloadTreeData[0].key as string] : []);
setExpandedTreeKeys(preloadTreeData ? getAllNodeKeys(preloadTreeData) : []);
reqBizGroupTree2(true, "", "", "", "", name, biz_reg_num, treeData, setTreeData);
}
else {
}
};
const handleClickTreeExpanded = (key: Key) => {
const node = findNodeByKey(treeData, key);
if (node.children == undefined)
reqBizGroupTree2(false, key as string, "", "", "", "", "", treeData, setTreeData);
}
const handleClickNode = (key: Key) => {
console.log("handleClickNode, ", key);
const node = findNodeByKey(treeData, key);
setFormInfo({name: node.name, biz_reg_num: node.biz_reg_num, email: node?.email ?? "", phone: node?.phone ?? "", ceo: node?.ceo ?? "", address: node?.address ?? ""});
}
const handleClickAdd = () => {
modalData.reset();
if (selectedTreeKeys.length > 0) {
modalData.set("pid", String(selectedTreeKeys[0]));
}
openModal();
};
const handleClickModify = () => {
if (selectedTreeKeys.length <= 0)
return;
const node = findNodeByKey(treeData, selectedTreeKeys[0]);
if (confirm("수정 하시겠습니까?")) {
reqModifyBizGroup(selectedTreeKeys[0]);
}
};
const handleClickDelete = () => {
if (selectedTreeKeys.length <= 0)
return;
const node = findNodeByKey(treeData, selectedTreeKeys[0]);
if (confirm("'" + node.title + "' 사업자를 삭제하시겠습니까?")) {
//const newTreeData = deleteNodeByKey(treeData, selectedTreeKeys[0]);
//setTreeData(newTreeData);
reqRemoveBizGroup(selectedTreeKeys[0]);
}
};
const handleModalOk = (insertedId: number) => {
closeModal();
const newNode = {
isModify: false,
key: String(insertedId),
//... title: modalData.values.name + "(" + modalData.values.biz_reg_num + ")",
title: modalData.values.name,
pid: modalData.values.pid,
name: modalData.values.name,
biz_reg_num: modalData.values.biz_reg_num,
phone: modalData.values.phone,
email: modalData.values.email,
ceo: modalData.values.ceo,
address: modalData.values.address,
isLeaf: true,
};
setTreeData(treeData => addNodeByKey(treeData, newNode.pid, newNode));
expandAndSelectTreeNode(treeData, selectedTreeKeys[0], setSelectedTreeKeys, expandedTreeKeys, setExpandedTreeKeys);
setExpandedTreeKeys((prev) => Array.from(new Set([...prev, ...selectedTreeKeys])));
setFormInfo({name: newNode.name, biz_reg_num: newNode.biz_reg_num, email: newNode.email, phone: newNode.phone, ceo: newNode.ceo, address: newNode.address});
};
const handleChangeFormBizRegNum = (value: string) => {
handleChangeFormInfo("biz_reg_num", formatBusinessNumber(value));
}
useEffect(() => {
//reqBizGroupTree2(true, '');
}, []);
return (
<div className="space-y-3">
<ComponentCard title={title} titleIcon={titleIcon}>
<div className="space-y-3">
<div className="grid grid-cols-1 gap-3 sm:grid-cols-4">
<WithLabel label="그룹명(사업자명)" label_width={SEARCH_LABEL_WIDTH}>
<Input type="text" placeholder="조회할 그룹명(사업자명)을 입력하세요." value={name} onChange={(e) => setName(e.target.value)}
onKeyDown={(e) => {if (e.key === "Enter") handleClickSearch()}} />
</WithLabel>
<WithLabel label="사업자번호" label_width={SEARCH_LABEL_WIDTH}>
<Input type="text" placeholder="조회할 사업자번호를 입력하세요." value={biz_reg_num} onChange={(e) => handleChangeBizRegNum(e.target.value)}
onKeyDown={(e) => {if (e.key === "Enter") handleClickSearch()}} />
</WithLabel>
<div className="flex items-center gap-5">
<Button size="sm" variant="primary" onClick={handleClickSearch}></Button>
</div>
</div>
{/*
<div className="space-y-2 grid grid-cols-1 gap-3 sm:grid-cols-2">
<WithLabel label="등록일" label_width={SEARCH_LABEL_WIDTH}>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
<DatePicker
id="date_start-picker"
placeholder="조회 시작일"
onChange={(dates, currentDateString) => { setDateStart(currentDateString); }}
/>
<DatePicker
id="date_end-picker"
placeholder="조회 종료일"
onChange={(dates, currentDateString) => { setDateEnd(currentDateString); }}
/>
</div>
</WithLabel>
<div className="flex items-center justify-end gap-5">
<Button size="sm" variant="primary" onClick={handleClickSearch}>조회</Button>
</div>
</div>
*/}
</div>
<div className="flex flex-col sm:flex-row gap-8">
<ComponentCard title="조직 계층도">
<div className="min-w-[500px] w-full h-[370px] overflow-auto">
<CtTree
treeData={treeData}
selectedKeys={selectedTreeKeys}
setSelectedKeys={setSelectedTreeKeys}
onClickNode={handleClickNode}
expandedKeys={expandedTreeKeys}
setExpandedKeys={setExpandedTreeKeys}
onClickExpanded={handleClickTreeExpanded}
/>
</div>
<div className="flex items-center gap-5 flex-wrap">
<p className="w-full mt-2">
<span className="relative before:content-['*'] before:text-red-500 before:mr-1"></span>
[] .<br />
<span className="relative before:content-['*'] before:text-red-500 before:mr-1"></span>
.
</p>
<Button size="sm" variant="primary" startIcon={<PlusIcon />} onClick={handleClickAdd}></Button>
<Button size="sm" variant="primary" startIcon={<TrashBinIcon />} onClick={handleClickDelete}></Button>
</div>
</ComponentCard>
<div className="w-[380px]">
<div className="flex-1 space-y-6">
<div>
<Label>()</Label>
<Input type="text" value={formInfo.name} onChange={(e) => handleChangeFormInfo("name", e.target.value)} />
</div>
<div>
<Label></Label>
<Input type="text" value={formInfo.biz_reg_num} onChange={(e) => handleChangeFormBizRegNum(e.target.value)} />
</div>
<div>
<Label></Label>
<Input type="text" value={formInfo.email} onChange={(e) => handleChangeFormInfo("email", e.target.value)} />
</div>
<div>
<Label></Label>
<Input type="text" value={formInfo.phone} onChange={(e) => handleChangeFormInfo("phone", e.target.value)} />
</div>
<div>
<Label></Label>
<Input type="text" value={formInfo.ceo} onChange={(e) => handleChangeFormInfo("ceo", e.target.value)} />
</div>
<div>
<Label></Label>
<Input type="text" value={formInfo.address} onChange={(e) => handleChangeFormInfo("address", e.target.value)} />
</div>
<Button size="sm" variant="primary" startIcon={<TrashBinIcon />} onClick={handleClickModify}></Button>
</div>
</div>
</div>
</ComponentCard>
<AddModifyBizGroupModal multiState={modalData} onOk={handleModalOk} isOpen={isOpen} closeModal={closeModal} />
</div>
);
}
@@ -0,0 +1,27 @@
"use client";
import React, { useState } from "react";
import CtModal from "@/components/CtModal";
import BizGroupContent from './BizGroupContent';
import { CtTreeNode } from "@/components/CtTree";
interface ManageBizGroupModalProps {
treeData: CtTreeNode[];
isOpen: boolean;
closeModal: () => void;
}
export default function ManageBizGroupModal({ treeData, isOpen, closeModal }: ManageBizGroupModalProps) {
return (
// <div className="rounded-2xl border border-gray-200 bg-white dark:border-gray-800 dark:bg-white/[0.03]">
<CtModal
title={"사업자정보 관리"}
description={"사업자 관련 정보 및 계층도를 관리합니다."}
isOpen={isOpen}
closeModal={closeModal}
className="max-w-[1280px] p-6 lg:p-10"
>
<BizGroupContent preloadTreeData={treeData} />
</CtModal>
// </div>
);
};
@@ -0,0 +1,15 @@
import BizGroup from './BizGroup';
import { Metadata } from "next";
import React from "react";
export const metadata: Metadata = {
title: "사업자원장 관리",
description: "사업자원장 관리",
};
export default function GroupPage() {
return (
<BizGroup />
);
}