대시보드, 기기에러, 계정관리 수정
This commit is contained in:
@@ -0,0 +1,180 @@
|
||||
"use client"
|
||||
import ComponentCard from '@/components/common/ComponentCard2';
|
||||
import PageBreadcrumb from "@/components/common/PageBreadCrumb2";
|
||||
import Badge from "@/components/ui/badge/Badge";
|
||||
import { BoxCubeIcon, DocsIcon, GroupIcon, PageIcon, PieChartIcon } from "@/icons";
|
||||
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import api, { convertDateTime3, useAuthStore } from '@/lib/_AG';
|
||||
|
||||
|
||||
const DASHBOARD_DAYS = 7;
|
||||
|
||||
type Column = {
|
||||
title: string;
|
||||
key: string;
|
||||
align?: "left" | "center" | "right";
|
||||
renderItem?: (item: any) => React.ReactNode;
|
||||
};
|
||||
|
||||
/** Small fixed table: the dashboard only shows one week, so there is no paging. */
|
||||
const DashTable = ({ columns, rows, emptyText }: { columns: Column[]; rows: any[]; emptyText: string }) => (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full min-w-[420px]">
|
||||
<thead>
|
||||
<tr className="border-b border-gray-200 dark:border-gray-800">
|
||||
{ columns.map((col, index) => (
|
||||
<th key={index} className={"px-3 py-2.5 text-theme-sm font-medium text-gray-600 dark:text-gray-400 text-" + (col.align ?? "center")}>
|
||||
{col.title}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{ rows.length === 0 &&
|
||||
<tr>
|
||||
<td colSpan={columns.length} className="px-3 py-10 text-center text-theme-sm text-gray-500 dark:text-gray-400">
|
||||
{emptyText}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
{ rows.map((row, rowIndex) => (
|
||||
<tr key={rowIndex} className="border-b border-gray-100 last:border-0 dark:border-gray-800">
|
||||
{ columns.map((col, colIndex) => (
|
||||
<td key={colIndex} className={"px-3 py-2.5 text-theme-sm text-gray-800 dark:text-white/90 text-" + (col.align ?? "center")}>
|
||||
{ col.renderItem ? col.renderItem(row) : (row[col.key] ?? "-") }
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
|
||||
const toNumber = (value: any) => Number(value ?? 0);
|
||||
const toComma = (value: any) => toNumber(value).toLocaleString("ko-KR");
|
||||
const toDate = (value: any) => (value ? String(value).substring(0, 10) : "-");
|
||||
|
||||
export default function Dashboard() {
|
||||
const router = useRouter();
|
||||
const tokenPayload = useAuthStore((s) => s.tokenPayload); //Bruce, JWT
|
||||
|
||||
const [ dateStart, setDateStart ] = useState<string>("");
|
||||
const [ dateEnd, setDateEnd ] = useState<string>("");
|
||||
const [ noticeList, setNoticeList ] = useState<any[]>([]);
|
||||
const [ salesList, setSalesList ] = useState<any[]>([]);
|
||||
const [ deviceErrorList, setDeviceErrorList ] = useState<any[]>([]);
|
||||
const [ accountList, setAccountList ] = useState<any[]>([]);
|
||||
const [ goodsList, setGoodsList ] = useState<any[]>([]);
|
||||
|
||||
function reqDashboard() {
|
||||
api.post('/api/get-dashboard.do', { days: DASHBOARD_DAYS }).then((resp) => {
|
||||
if (resp.data.errCode != 0) {
|
||||
alert("일시적으로 사용할 수 없습니다 : " + resp.data.errCode);
|
||||
return;
|
||||
}
|
||||
|
||||
const result = resp.data.result;
|
||||
setDateStart(result.date_start ?? "");
|
||||
setDateEnd(result.date_end ?? "");
|
||||
setNoticeList(result.notice_list ?? []);
|
||||
setSalesList(result.sales_list ?? []);
|
||||
setDeviceErrorList(result.device_error_list ?? []);
|
||||
setAccountList(result.account_list ?? []);
|
||||
setGoodsList(result.goods_list ?? []);
|
||||
})
|
||||
.catch((err) => console.error(err));
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (tokenPayload == undefined)
|
||||
return;
|
||||
|
||||
reqDashboard();
|
||||
}, [tokenPayload]);
|
||||
|
||||
|
||||
const salesColumns: Column[] = [
|
||||
{ title: "날짜", key: "date", renderItem: (item) => item.date },
|
||||
{ title: "승인", key: "approval_count", align: "right", renderItem: (item) => toComma(item.approval_count) + "건" },
|
||||
{ title: "취소", key: "cancel_count", align: "right", renderItem: (item) => toComma(item.cancel_count) + "건" },
|
||||
{ title: "전체", key: "total_count", align: "right", renderItem: (item) => toComma(item.total_count) + "건" },
|
||||
{ title: "금액", key: "total_amount", align: "right", renderItem: (item) => toComma(item.total_amount) + "원" },
|
||||
];
|
||||
|
||||
const deviceErrorColumns: Column[] = [
|
||||
{ title: "날짜", key: "date", renderItem: (item) => item.date },
|
||||
{
|
||||
title: "단말기 이상", key: "fault_count", align: "right",
|
||||
renderItem: (item) => toNumber(item.fault_count) > 0
|
||||
? <Badge size="sm" color="error">{toComma(item.fault_count)}건</Badge>
|
||||
: "0건",
|
||||
},
|
||||
{
|
||||
title: "품절", key: "sold_out_count", align: "right",
|
||||
renderItem: (item) => toNumber(item.sold_out_count) > 0
|
||||
? <Badge size="sm" color="warning">{toComma(item.sold_out_count)}건</Badge>
|
||||
: "0건",
|
||||
},
|
||||
{ title: "합계", key: "total_count", align: "right", renderItem: (item) => toComma(item.total_count) + "건" },
|
||||
];
|
||||
|
||||
const noticeColumns: Column[] = [
|
||||
{ title: "등록일", key: "reg_time", renderItem: (item) => convertDateTime3(item.reg_time) },
|
||||
{ title: "제목", key: "title", align: "left" },
|
||||
];
|
||||
|
||||
const accountColumns: Column[] = [
|
||||
{ title: "등록일", key: "reg_time", renderItem: (item) => toDate(item.reg_time) },
|
||||
{ title: "아이디", key: "user_id" },
|
||||
{ title: "이름", key: "nick_name", renderItem: (item) => item.nick_name ?? item.name ?? "-" },
|
||||
{ title: "그룹(사업자)", key: "biz_group_name", align: "left" },
|
||||
];
|
||||
|
||||
const goodsColumns: Column[] = [
|
||||
{ title: "등록일", key: "reg_time", renderItem: (item) => toDate(item.reg_time) },
|
||||
{ title: "상품코드", key: "code" },
|
||||
{ title: "상품명", key: "name", align: "left" },
|
||||
{ title: "가격", key: "price", align: "right", renderItem: (item) => toComma(item.price) + "원" },
|
||||
];
|
||||
|
||||
const period = dateStart && dateEnd ? dateStart + " ~ " + dateEnd : "";
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PageBreadcrumb pageTitle1="홈 (대시보드)" />
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="rounded-lg border border-gray-200 bg-gray-50 px-4 py-3 text-sm text-gray-600 dark:border-gray-800 dark:bg-white/[0.03] dark:text-gray-400">
|
||||
최근 {DASHBOARD_DAYS}일간의 일별 현황입니다. { period && <span className="font-medium text-gray-800 dark:text-white/90">({period})</span> }
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-3 xl:grid-cols-2">
|
||||
<ComponentCard title="매출집계 (일별)" titleIcon={<PieChartIcon />}>
|
||||
<DashTable columns={salesColumns} rows={salesList} emptyText="기간 내 거래내역이 없습니다." />
|
||||
</ComponentCard>
|
||||
|
||||
<ComponentCard title="무인기기 ERROR/품절내역 (일별)" titleIcon={<BoxCubeIcon />}>
|
||||
<DashTable columns={deviceErrorColumns} rows={deviceErrorList} emptyText="기간 내 이상/품절 내역이 없습니다." />
|
||||
</ComponentCard>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-3 xl:grid-cols-2">
|
||||
<ComponentCard title="추가된 계정" titleIcon={<GroupIcon />}>
|
||||
<DashTable columns={accountColumns} rows={accountList} emptyText="기간 내 추가된 계정이 없습니다." />
|
||||
</ComponentCard>
|
||||
|
||||
<ComponentCard title="추가된 상품" titleIcon={<DocsIcon />}>
|
||||
<DashTable columns={goodsColumns} rows={goodsList} emptyText="기간 내 추가된 상품이 없습니다." />
|
||||
</ComponentCard>
|
||||
</div>
|
||||
|
||||
<ComponentCard title="공지사항" titleIcon={<PageIcon />}>
|
||||
<DashTable columns={noticeColumns} rows={noticeList} emptyText="기간 내 등록된 공지사항이 없습니다." />
|
||||
</ComponentCard>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -23,6 +23,26 @@ import Select from "@/components/form/Select2";
|
||||
import PageCountSelector from "@/components/PageCountSelector";
|
||||
|
||||
|
||||
const ACCOUNT_STATE_LABEL: { [key: string]: string } = {
|
||||
"1": "정상", "2": "탈퇴", "3": "휴면", "4": "정지", "5": "잠김",
|
||||
};
|
||||
|
||||
const ACCOUNT_STATE_OPTIONS = [
|
||||
{ label: "정상", value: "1" },
|
||||
{ label: "탈퇴", value: "2" },
|
||||
{ label: "휴면", value: "3" },
|
||||
{ label: "정지", value: "4" },
|
||||
{ label: "잠김", value: "5" },
|
||||
];
|
||||
|
||||
const accountStateColor = (state: any) => {
|
||||
const value = Number(state);
|
||||
if (value === 1) return "success";
|
||||
if (value === 2) return "light";
|
||||
if (value === 4 || value === 5) return "error";
|
||||
return "warning";
|
||||
};
|
||||
|
||||
const SEARCH_LABEL_WIDTH_PX = 80;
|
||||
//const COUNT_PER_PAGE = 10;
|
||||
const PAGE_NUM_COUNT = 5;
|
||||
@@ -49,6 +69,7 @@ export default function Account() {
|
||||
const [ user_id, setUserId ] = useState<string>("");
|
||||
const [ email, setEmail ] = useState<string>("");
|
||||
const [ nick_name, setNickName ] = useState<string>("");
|
||||
const [ state, setState ] = useState<string>("");
|
||||
const [ date_start, setDateStart ] = useState<string>();
|
||||
const [ date_end, setDateEnd ] = useState<string>();
|
||||
|
||||
@@ -70,6 +91,7 @@ export default function Account() {
|
||||
user_id: user_id,
|
||||
email: email,
|
||||
nick_name: nick_name,
|
||||
state: state,
|
||||
date_start: date_start,
|
||||
date_end: date_end,
|
||||
biz_group_id: biz_group_id,
|
||||
@@ -200,8 +222,8 @@ export default function Account() {
|
||||
openModal();
|
||||
};
|
||||
|
||||
const handleRowClickDelete = (row: number) => {
|
||||
if (confirm("'" + tableData[row].user_id + "' 계정을 삭제하시겠습니까?")) {
|
||||
const handleRowClickTerminate = (row: number) => {
|
||||
if (confirm("'" + tableData[row].user_id + "' 계정을 해지하시겠습니까?")) {
|
||||
reqRemoveAccount(tableData[row].gid);
|
||||
}
|
||||
};
|
||||
@@ -251,8 +273,8 @@ export default function Account() {
|
||||
title: "상태",
|
||||
key: "state",
|
||||
renderItem: (item : any) => (
|
||||
<Badge size="sm" color={item.state === 1 ? "success" : item.state === 4 ? "error" : "warning"}>
|
||||
{item.state === 1 ? "정상" : "정지"}
|
||||
<Badge size="sm" color={accountStateColor(item.state)}>
|
||||
{ACCOUNT_STATE_LABEL[String(item.state)] ?? ""}
|
||||
</Badge>
|
||||
)
|
||||
},
|
||||
@@ -268,13 +290,13 @@ export default function Account() {
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "삭제",
|
||||
title: "해지",
|
||||
key: "_",
|
||||
renderItem: (item: any, row: number) => (
|
||||
<div>
|
||||
<TrashBinIcon
|
||||
className="cursor-pointer hover:fill-error-500 dark:hover:fill-error-500 fill-gray-700 dark:fill-gray-400"
|
||||
onClick={() => {handleRowClickDelete(row)}} />
|
||||
onClick={() => {handleRowClickTerminate(row)}} />
|
||||
</div>
|
||||
),
|
||||
},
|
||||
@@ -323,7 +345,7 @@ export default function Account() {
|
||||
<ComponentCard title="계정 목록 조회" titleIcon={<TableIcon />}>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-3">
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-4">
|
||||
<WithLabel label="아이디" label_width={SEARCH_LABEL_WIDTH_PX}>
|
||||
<Input type="text" placeholder="조회할 아이디를 입력하세요." value={user_id} onChange={(e) => setUserId(e.target.value)} />
|
||||
</WithLabel>
|
||||
@@ -333,6 +355,14 @@ export default function Account() {
|
||||
<WithLabel label="닉네임" label_width={SEARCH_LABEL_WIDTH_PX}>
|
||||
<Input type="text" placeholder="조회할 닉네임을 입력하세요." value={nick_name} onChange={(e) => setNickName(e.target.value)} />
|
||||
</WithLabel>
|
||||
<WithLabel label="상태" label_width={SEARCH_LABEL_WIDTH_PX}>
|
||||
<Select
|
||||
placeholder="전체"
|
||||
options={ACCOUNT_STATE_OPTIONS}
|
||||
defaultValue={state}
|
||||
onChange={(e) => setState(e.target.value)}
|
||||
className="dark:bg-dark-900" />
|
||||
</WithLabel>
|
||||
</div>
|
||||
<div className="space-y-2 grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||
<WithLabel label="등록일" label_width={50}>
|
||||
|
||||
@@ -154,7 +154,10 @@ export default function AddModifyAccountModal({ multiState, onOk, isOpen, closeM
|
||||
placeholder="선택하세요"
|
||||
options={[
|
||||
{ label: "정상", value: "1" },
|
||||
{ label: "탈퇴", value: "2" },
|
||||
{ label: "휴면", value: "3" },
|
||||
{ label: "정지", value: "4" },
|
||||
{ label: "잠김", value: "5" },
|
||||
]}
|
||||
defaultValue={multiState.values.state} onChange={(e) => multiState.set("state", e.target.value)}
|
||||
className="dark:bg-dark-900" />
|
||||
|
||||
@@ -0,0 +1,435 @@
|
||||
"use client"
|
||||
import PageBreadcrumb from "@/components/common/PageBreadCrumb2";
|
||||
import ComponentCard from '@/components/common/ComponentCard2';
|
||||
import BizGroupPanel from "@/components/BizGroupPanel";
|
||||
import CtTable1 from '@/components/tables/CtTable1';
|
||||
import Button from '@/components/ui/button/Button2';
|
||||
import { DocsIcon, TableIcon, FilterIcon } from "@/icons";
|
||||
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, { addDaysYMD, convertDateTime3, postFileDownload, todayYMD, useAuthStore, useUiLoadingStore } from '@/lib/_AG';
|
||||
import CtTree, { CtTreeNode, findNodeByKey, findRootNodeByKey } from "@/components/CtTree";
|
||||
import { reqBizGroupTree2, reqBizGroupTree3 } from "../biz-group/BizGroupContent";
|
||||
import Checkbox from "@/components/form/input/Checkbox";
|
||||
import CtSelectedTextField from "@/components/CtSelectedText";
|
||||
import CtDrawer from "@/components/CtDrawer";
|
||||
import { useModal } from "@/hooks/useModal";
|
||||
import { useMultiState } from "@/hooks/useMultiState";
|
||||
import ErrorDetailModal, { ErrorDetailModalProps, DeviceErrorDetailItem } from "./ErrorDetailModal";
|
||||
import PageCountSelector from "@/components/PageCountSelector";
|
||||
import InputSelectField from "@/components/form/input/InputSelectField";
|
||||
import Select from "@/components/form/Select2";
|
||||
import { deviceErrorTypeLabel } from "@/lib/deviceErrorCode";
|
||||
|
||||
|
||||
const SEARCH_LABEL_WIDTH_PX = 80;
|
||||
//const COUNT_PER_PAGE = 10;
|
||||
const PAGE_NUM_COUNT = 5;
|
||||
|
||||
|
||||
export default function DeviceError() {
|
||||
const tokenPayload = useAuthStore((s) => s.tokenPayload); //Bruce, JWT
|
||||
const isLoadingGlobal = useUiLoadingStore((s) => s.isLoading);
|
||||
|
||||
const [ tableData, setTableData ] = useState<any[]>([]);
|
||||
const [ tableOffset, setTableOffset ] = useState<number>(0);
|
||||
const [ tableTotalCount, setTableTotalCount ] = useState<number>(0);
|
||||
|
||||
const [ COUNT_PER_PAGE, setCOUNT_PER_PAGE ] = useState<number>(20);
|
||||
|
||||
const [ treeData, setTreeData ] = useState<CtTreeNode[]>([]);
|
||||
const [ selectedTreeKeys, setSelectedTreeKeys ] = useState<Key[]>([]);
|
||||
const [ expandedTreeKeys, setExpandedTreeKeys ] = useState<Key[]>([]);
|
||||
const [ isGroupAccess, setGroupAccess ] = useState(tokenPayload?.permission == 0xffffffff); //TempCode
|
||||
const [ isTreeOpen, setTreeOpen ] = useState(tokenPayload?.permission == 0xffffffff); //TempCode
|
||||
|
||||
// group search fields
|
||||
const [ name, setName ] = useState<string>("");
|
||||
const [ biz_reg_num, setBizRegNum ] = useState<string>("");
|
||||
|
||||
//search fields
|
||||
const [ device_name, setDeviceName ] = useState<string>("");
|
||||
const [ uid1, setUid1 ] = useState<string>("");
|
||||
const [ list_uid1, setListUid1 ] = useState<{label: string; value: string}[]>([]);
|
||||
const [ error_type, setErrorType ] = useState<string>("");
|
||||
const [ date_start, setDateStart ] = useState<string>(addDaysYMD(-7));
|
||||
const [ date_end, setDateEnd ] = useState<string>(todayYMD());
|
||||
|
||||
const { isOpen, openModal, closeModal } = useModal();
|
||||
const [ modalItems, setModalItems ] = useState<DeviceErrorDetailItem[]>([]);
|
||||
const modalData = useMultiState<ErrorDetailModalProps>({device_error_id: 0, reg_time: "", device_name: "", uid1: "", uid1_type: 0, type: "", error_type: 0, error_code: "", biz_group_name: ""});
|
||||
|
||||
const [ isMobileMode, setIsMobileMode ] = useState<boolean>(false);
|
||||
const [ isSearchFieldOpen, setIsSearchFieldOpen ] = useState<boolean>(false);
|
||||
|
||||
const [ listSelectedText, setListSelectedText ] = useState<string[]>([]);
|
||||
const changeListSelectedText = (index: number, text: string) => {
|
||||
setListSelectedText(prev => {
|
||||
const copy = [...prev];
|
||||
copy[index] = text;
|
||||
return copy;
|
||||
});
|
||||
}
|
||||
|
||||
//function part
|
||||
//
|
||||
function reqUid1List() {
|
||||
api.post('/api/list-uid1.do').then((resp) => {
|
||||
console.log(resp);
|
||||
|
||||
if (resp.data.errCode == 0) {
|
||||
if (resp.data.result.list.length == 0) {
|
||||
setListUid1([]);
|
||||
return;
|
||||
}
|
||||
|
||||
setListUid1(
|
||||
resp.data.result.list.map((item: { uid1: string; name: string }) => ({
|
||||
label: item.uid1 + (item.name !== "" ? " (" + item.name + ")" : ""),
|
||||
value: item.uid1,
|
||||
}))
|
||||
);
|
||||
}
|
||||
})
|
||||
.catch((err) => console.error(err))
|
||||
}
|
||||
|
||||
function reqDeviceErrorDetail(device_error_id: number) {
|
||||
api.post('/api/get-device-error-detail.do', { device_error_id: device_error_id }).then((resp) => {
|
||||
if (resp.data.errCode != 0) {
|
||||
alert("일시적으로 사용할 수 없습니다 : " + resp.data.errCode);
|
||||
return;
|
||||
}
|
||||
|
||||
setModalItems(resp.data.result.list ?? []);
|
||||
})
|
||||
.catch((err) => console.error(err));
|
||||
}
|
||||
|
||||
function reqDeviceErrorList(is_excel: boolean, biz_group_id: string, groupAccess: boolean) {
|
||||
if (!biz_group_id)
|
||||
return;
|
||||
|
||||
const params = {
|
||||
is_group_access: groupAccess,
|
||||
is_excel: is_excel,
|
||||
offset: tableOffset,
|
||||
limit: COUNT_PER_PAGE,
|
||||
device_name: device_name,
|
||||
uid1: uid1,
|
||||
error_type: error_type,
|
||||
date_start: date_start,
|
||||
date_end: date_end,
|
||||
biz_group_id: biz_group_id,
|
||||
}
|
||||
|
||||
if (is_excel == true) {
|
||||
postFileDownload('/api/get-device-error.do', params);
|
||||
return;
|
||||
}
|
||||
|
||||
api.post('/api/get-device-error.do', params).then((resp) => {
|
||||
console.log(resp);
|
||||
|
||||
if (resp.data.errCode === 0) {
|
||||
if (resp.data.result.list.length == 0) {
|
||||
alert("조회 결과가 없습니다.");
|
||||
setTableTotalCount(0);
|
||||
setTableData([]);
|
||||
return;
|
||||
}
|
||||
setTableTotalCount(resp.data.result.totalCount);
|
||||
setTableData(resp.data.result.list);
|
||||
}
|
||||
else {
|
||||
alert("일시적으로 사용할 수 없습니다 : " + resp.data.errCode);
|
||||
}
|
||||
})
|
||||
.catch((err) => console.error(err));
|
||||
}
|
||||
|
||||
|
||||
// event processing part
|
||||
//
|
||||
// tree part //////////
|
||||
const handleClickGroupSearch = () => {
|
||||
if (tokenPayload?.permission == 0xffffffff) {
|
||||
if (name == "" && biz_reg_num == "") {
|
||||
alert("그룹(사업자)명 또는 사업자등록번호를 입력하셔야 합니다.");
|
||||
return;
|
||||
}
|
||||
if (name != "" && name.length < 2) {
|
||||
alert("그룹(사업자)명은 두글자 이상 입력해야 합니다.");
|
||||
return;
|
||||
}
|
||||
if (biz_reg_num != "" && biz_reg_num.length < 2) {
|
||||
alert("사업자등록번호는 두글자 이상 입력해야 합니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
reqBizGroupTree2(true, "", "", "", "", name, biz_reg_num, treeData, setTreeData);
|
||||
}
|
||||
else {
|
||||
reqBizGroupTree3(String(tokenPayload?.biz_group_id), setTreeData, setSelectedTreeKeys, setExpandedTreeKeys, (resp: any) => {
|
||||
reqUid1List();
|
||||
|
||||
reqDeviceErrorList(false, String(resp.data.result.tree[0].key), isGroupAccess);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleClickNode = (key: Key) => {
|
||||
if (isGroupAccess == false) {
|
||||
const root = findRootNodeByKey(treeData, key as string);
|
||||
reqDeviceErrorList(false, root.key as string, isGroupAccess);
|
||||
return;
|
||||
}
|
||||
|
||||
setTableOffset(0);
|
||||
reqDeviceErrorList(false, key as string, isGroupAccess);
|
||||
}
|
||||
|
||||
const handleClickTreeExpanded = (key: Key) => {
|
||||
const node = findNodeByKey(treeData, key);
|
||||
if (node.children == undefined)
|
||||
reqBizGroupTree2(false, key as string, "", "", "", "", "2", treeData, setTreeData);
|
||||
}
|
||||
|
||||
const handleClickGroupAccess = (value: boolean) => {
|
||||
setGroupAccess(value);
|
||||
|
||||
if (tokenPayload?.permission == 0xffffffff) {
|
||||
if (selectedTreeKeys.length <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
reqDeviceErrorList(false, selectedTreeKeys[0] as string, value);
|
||||
return;
|
||||
}
|
||||
|
||||
if (value == false || selectedTreeKeys.length <= 0) {
|
||||
reqDeviceErrorList(false, String(tokenPayload?.biz_group_id), value);
|
||||
}
|
||||
else {
|
||||
reqDeviceErrorList(false, selectedTreeKeys[0] as string, value);
|
||||
}
|
||||
}
|
||||
// tree part //////////
|
||||
|
||||
const handleClickDownload = () => {
|
||||
reqDeviceErrorList(true, selectedTreeKeys[0] as string, isGroupAccess);
|
||||
};
|
||||
|
||||
const handleClickSearch = () => {
|
||||
if (selectedTreeKeys.length <= 0) {
|
||||
alert("조회하려면 사업자를 선택해야 합니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
reqDeviceErrorList(false, selectedTreeKeys[0] as string, isGroupAccess);
|
||||
};
|
||||
|
||||
const handlePageChange = (page: number) => {
|
||||
setTableOffset((page - 1) * COUNT_PER_PAGE);
|
||||
};
|
||||
|
||||
const handleRowClickErrorDetail = (row: number) => {
|
||||
// The group name is only shown as context, so a missing tree node must not block the detail
|
||||
const selectedKey = selectedTreeKeys[0];
|
||||
const node = selectedKey != undefined ? findRootNodeByKey(treeData, String(selectedKey)) : null;
|
||||
|
||||
modalData.setAll({...tableData[row],
|
||||
biz_group_name: node?.name ?? "",
|
||||
});
|
||||
|
||||
// The telegram is parsed on the server, which also names the goods of every sold out column
|
||||
setModalItems([]);
|
||||
reqDeviceErrorDetail(tableData[row].device_error_id);
|
||||
|
||||
openModal();
|
||||
};
|
||||
|
||||
const handleModalOk = () => {
|
||||
closeModal();
|
||||
}
|
||||
//
|
||||
const handleChangeDeviceName = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setDeviceName(e.target.value);
|
||||
changeListSelectedText(0, e.target.value);
|
||||
}
|
||||
|
||||
const handleChangeUid1 = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setUid1(e.target.value);
|
||||
changeListSelectedText(1, e.target.value);
|
||||
}
|
||||
|
||||
const handleChangeErrorType = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
setErrorType(e.target.value);
|
||||
changeListSelectedText(2, e.target.options[e.target.selectedIndex].text);
|
||||
}
|
||||
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (tokenPayload == undefined)
|
||||
return;
|
||||
|
||||
if (tokenPayload?.permission == 0xffffffff)
|
||||
return;
|
||||
|
||||
reqBizGroupTree3(String(tokenPayload?.biz_group_id), setTreeData, setSelectedTreeKeys, setExpandedTreeKeys, (resp: any) => {
|
||||
reqUid1List();
|
||||
|
||||
setGroupAccess(tokenPayload?.permission == 0xffffffff);
|
||||
setTreeOpen(tokenPayload?.permission == 0xffffffff);
|
||||
|
||||
reqDeviceErrorList(false, String(resp.data.result.tree[0].key), tokenPayload?.permission == 0xffffffff);
|
||||
});
|
||||
}, [tokenPayload]);
|
||||
|
||||
useEffect(() => {
|
||||
reqDeviceErrorList(false, selectedTreeKeys[0] as string, isGroupAccess);
|
||||
}, [tableOffset]);
|
||||
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: "발생시간",
|
||||
key: "reg_time",
|
||||
renderItem: (item: any, row: number) => convertDateTime3(item.reg_time)
|
||||
},
|
||||
{
|
||||
title: "무인기기명",
|
||||
key: "device_name",
|
||||
},
|
||||
{
|
||||
title: "단말기 TID",
|
||||
key: "uid1",
|
||||
renderItem: (item: any, row: number) => item.uid1 ? (item.uid1 + " (" + (item.uid1_type == 1 ? "KICC" : "NICE") + ")") : "",
|
||||
viewMobile: true,
|
||||
},
|
||||
{
|
||||
title: "에러유형",
|
||||
key: "error_type",
|
||||
renderItem: (item: any, row: number) => deviceErrorTypeLabel(item.error_type),
|
||||
viewMobile: true,
|
||||
},
|
||||
{
|
||||
title: "에러내용",
|
||||
key: "error_desc",
|
||||
viewMobile: true,
|
||||
},
|
||||
{
|
||||
title: "상세내역",
|
||||
key: "_",
|
||||
renderItem: (item: any, row: number) => (
|
||||
<button className="inline-flex items-center px-1 py-0.5 justify-center gap-1 rounded font-medium text-theme-xs bg-error-50 text-error-600 dark:bg-error-500/15 dark:text-error-500"
|
||||
onClick={() => {handleRowClickErrorDetail(row)}}>
|
||||
상세정보
|
||||
</button>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PageBreadcrumb pageTitle1="무인기기 ERROR/품절내역" />
|
||||
|
||||
<div className="space-y-3">
|
||||
<BizGroupPanel
|
||||
title="사업자 그룹별로 관리"
|
||||
isOpen={isTreeOpen}
|
||||
setOpen={setTreeOpen}
|
||||
name={name}
|
||||
bizRegNum={biz_reg_num}
|
||||
onChangeName={setName}
|
||||
onChangeBizRegNum={setBizRegNum}
|
||||
onClickSearch={handleClickGroupSearch}
|
||||
isDisabled={isLoadingGlobal}
|
||||
searchLabelWidth={SEARCH_LABEL_WIDTH_PX}
|
||||
>
|
||||
<CtTree
|
||||
treeData={treeData}
|
||||
selectedKeys={selectedTreeKeys}
|
||||
setSelectedKeys={setSelectedTreeKeys}
|
||||
onClickNode={handleClickNode}
|
||||
expandedKeys={expandedTreeKeys}
|
||||
setExpandedKeys={setExpandedTreeKeys}
|
||||
onClickExpanded={handleClickTreeExpanded}
|
||||
/>
|
||||
</BizGroupPanel>
|
||||
|
||||
<ComponentCard title="ERROR/품절내역 목록" titleIcon={<TableIcon />}>
|
||||
|
||||
<CtDrawer title="상세 조회" isMobileMode={isMobileMode} setIsMobileMode={setIsMobileMode} isSearchFieldOpen={isSearchFieldOpen} setIsSearchFieldOpen={setIsSearchFieldOpen}>
|
||||
<div className="space-y-3">
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-4">
|
||||
<WithLabel label="무인기기명" label_width={SEARCH_LABEL_WIDTH_PX}>
|
||||
<Input type="text" placeholder="무인기기명을 입력하세요." value={device_name} onChange={handleChangeDeviceName} />
|
||||
</WithLabel>
|
||||
<WithLabel label="단말기 TID" label_width={SEARCH_LABEL_WIDTH_PX}>
|
||||
<InputSelectField type="text" placeholder="TID를 입력하세요." value={uid1} options={list_uid1} onChange={handleChangeUid1} />
|
||||
</WithLabel>
|
||||
<WithLabel label="에러유형" label_width={SEARCH_LABEL_WIDTH_PX}>
|
||||
<Select
|
||||
placeholder="전체"
|
||||
options={[
|
||||
{ label: "단말기 이상", value: "1" },
|
||||
{ label: "품절", value: "2" },
|
||||
]}
|
||||
defaultValue={error_type} onChange={handleChangeErrorType}
|
||||
className="dark:bg-dark-900" />
|
||||
</WithLabel>
|
||||
</div>
|
||||
</div>
|
||||
</CtDrawer>
|
||||
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||
<WithLabel label="발생일" label_width={50}>
|
||||
<div className="grid grid-cols-2 gap-3 sm:grid-cols-2">
|
||||
<DatePicker
|
||||
id="date_start-picker"
|
||||
placeholder="조회 시작일"
|
||||
onChange={(dates, currentDateString) => { setDateStart(currentDateString); }}
|
||||
defaultDate={date_start}
|
||||
/>
|
||||
<DatePicker
|
||||
id="date_end-picker"
|
||||
placeholder="조회 종료일"
|
||||
onChange={(dates, currentDateString) => { setDateEnd(currentDateString); }}
|
||||
defaultDate={date_end}
|
||||
/>
|
||||
</div>
|
||||
</WithLabel>
|
||||
<div className="flex items-center justify-end gap-5">
|
||||
<Button size="sm" variant="outline" startIcon={<DocsIcon />} onClick={handleClickDownload}>엑셀파일 다운로드</Button>
|
||||
{ isMobileMode && <Button size="sm" variant="outline" startIcon={<FilterIcon />} onClick={() => setIsSearchFieldOpen(true)}>필터</Button> }
|
||||
<Button size="sm" variant="primary" onClick={handleClickSearch} disabled={isLoadingGlobal}>조회</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-end gap-5">
|
||||
{ isTreeOpen && <Checkbox checked={isGroupAccess} onChange={(value) => handleClickGroupAccess(value)} label="그룹단위로 조회" /> }
|
||||
<PageCountSelector defaultValue={COUNT_PER_PAGE.toString()} onChange={(e) => setCOUNT_PER_PAGE(parseInt(e.target.value))} />
|
||||
</div>
|
||||
|
||||
{ isMobileMode &&
|
||||
<CtSelectedTextField label={"필터:"} listText={listSelectedText} setListText={setListSelectedText}></CtSelectedTextField>
|
||||
}
|
||||
|
||||
<CtTable1 columns={columns} bodyData={tableData}
|
||||
offset={tableOffset} totalCount={tableTotalCount} countPerPage={COUNT_PER_PAGE} pageNumCount={PAGE_NUM_COUNT}
|
||||
onClickPageChange={handlePageChange} />
|
||||
|
||||
</ComponentCard>
|
||||
|
||||
<ErrorDetailModal multiState={modalData} items={modalItems} onOk={handleModalOk} isOpen={isOpen} closeModal={closeModal} />
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
"use client";
|
||||
import React from "react";
|
||||
import CtModal from "@/components/CtModal";
|
||||
import Badge from "@/components/ui/badge/Badge";
|
||||
import { useMultiState } from "@/hooks/useMultiState";
|
||||
|
||||
|
||||
export interface ErrorDetailModalProps {
|
||||
device_error_id: number;
|
||||
reg_time: string;
|
||||
device_name: string;
|
||||
uid1: string;
|
||||
uid1_type: number;
|
||||
type: string;
|
||||
error_type: number;
|
||||
error_code: string;
|
||||
biz_group_name: string;
|
||||
}
|
||||
|
||||
/** One decoded line of the telegram, built by /api/get-device-error-detail.do */
|
||||
export interface DeviceErrorDetailItem {
|
||||
detail: string;
|
||||
goods_code: string | null;
|
||||
goods_name: string | null;
|
||||
state: string;
|
||||
}
|
||||
|
||||
|
||||
interface ViewErrorDetailModalProps {
|
||||
multiState: ReturnType<typeof useMultiState<ErrorDetailModalProps>>;
|
||||
items: DeviceErrorDetailItem[];
|
||||
onOk: () => void;
|
||||
isOpen: boolean;
|
||||
closeModal: () => void;
|
||||
}
|
||||
|
||||
const pad2 = (value: number) => String(value).padStart(2, "0");
|
||||
|
||||
const splitRegTime = (reg_time: string) => {
|
||||
if (!reg_time)
|
||||
return { date: "", time: "" };
|
||||
|
||||
const d = new Date(reg_time);
|
||||
if (isNaN(d.getTime()))
|
||||
return { date: reg_time, time: "" };
|
||||
|
||||
return {
|
||||
date: d.getFullYear() + "-" + pad2(d.getMonth() + 1) + "-" + pad2(d.getDate()),
|
||||
time: pad2(d.getHours()) + ":" + pad2(d.getMinutes()) + ":" + pad2(d.getSeconds()),
|
||||
};
|
||||
};
|
||||
|
||||
const goodsText = (item: DeviceErrorDetailItem) => {
|
||||
if (!item.goods_name)
|
||||
return "-";
|
||||
|
||||
return item.goods_name + (item.goods_code ? " (" + item.goods_code + ")" : "");
|
||||
};
|
||||
|
||||
const HeaderField = ({ label, value }: { label: string; value: React.ReactNode }) => (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="shrink-0 text-sm text-gray-500 dark:text-gray-400">{label}</span>
|
||||
<span className="min-w-0 flex-1 truncate rounded-lg border border-gray-200 bg-gray-50 px-3 py-1.5 text-sm text-gray-800 dark:border-gray-800 dark:bg-white/[0.03] dark:text-white/90">
|
||||
{value !== null && value !== undefined && value !== "" ? value : "-"}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default function ErrorDetailModal({ multiState, items, onOk, isOpen, closeModal }: ViewErrorDetailModalProps) {
|
||||
const values = multiState.values;
|
||||
const { date, time } = splitRegTime(values.reg_time);
|
||||
|
||||
return (
|
||||
<CtModal
|
||||
title="자판기 상세내역"
|
||||
isOpen={isOpen}
|
||||
closeModal={closeModal}
|
||||
className="max-w-[700px] p-6 lg:p-10"
|
||||
>
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||
<HeaderField label="발생일자" value={date} />
|
||||
<HeaderField label="발생시간" value={time} />
|
||||
<HeaderField label="무인기기명" value={values.device_name} />
|
||||
<HeaderField label="단말기 TID" value={values.uid1 ? values.uid1 + (values.uid1_type ? " (" + (values.uid1_type == 1 ? "KICC" : "NICE") + ")" : "") : ""} />
|
||||
<HeaderField label="그룹(사업자)" value={values.biz_group_name} />
|
||||
</div>
|
||||
|
||||
<div className="mt-4 overflow-hidden rounded-lg border border-gray-200 dark:border-gray-800">
|
||||
<div className="max-h-[320px] overflow-y-auto custom-scrollbar">
|
||||
<table className="w-full">
|
||||
<thead className="sticky top-0 bg-gray-50 dark:bg-white/[0.03]">
|
||||
<tr>
|
||||
<th className="border-b border-gray-200 px-4 py-2.5 text-center text-theme-sm font-medium text-gray-700 dark:border-gray-800 dark:text-gray-300">상세 내역</th>
|
||||
<th className="border-b border-gray-200 px-4 py-2.5 text-center text-theme-sm font-medium text-gray-700 dark:border-gray-800 dark:text-gray-300">상품명(상품코드)</th>
|
||||
<th className="w-28 border-b border-gray-200 px-4 py-2.5 text-center text-theme-sm font-medium text-gray-700 dark:border-gray-800 dark:text-gray-300">이상 유무</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{ items.length === 0 &&
|
||||
<tr>
|
||||
<td colSpan={3} className="px-4 py-10 text-center text-theme-sm text-gray-500 dark:text-gray-400">
|
||||
이상 내역이 없습니다.
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
{ items.map((item, index) => (
|
||||
<tr key={index} className="border-b border-gray-100 last:border-0 dark:border-gray-800">
|
||||
<td className="px-4 py-2.5 text-center text-theme-sm text-gray-800 dark:text-white/90">{item.detail}</td>
|
||||
<td className="px-4 py-2.5 text-center text-theme-sm text-gray-600 dark:text-gray-400">{goodsText(item)}</td>
|
||||
<td className="px-4 py-2.5 text-center">
|
||||
<Badge size="sm" color={item.state === "품절" ? "warning" : "error"}>{item.state}</Badge>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</CtModal>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
import DeviceError from './DeviceError';
|
||||
import { Metadata } from "next";
|
||||
import React from "react";
|
||||
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "무인기기 ERROR/품절내역",
|
||||
description: "무인기기 ERROR/품절내역",
|
||||
};
|
||||
|
||||
export default function DeviceErrorPage() {
|
||||
return (
|
||||
<DeviceError />
|
||||
);
|
||||
}
|
||||
@@ -1,11 +1,6 @@
|
||||
import type { Metadata } from "next";
|
||||
import { EcommerceMetrics } from "@/components/ecommerce/EcommerceMetrics";
|
||||
import React from "react";
|
||||
import MonthlyTarget from "@/components/ecommerce/MonthlyTarget";
|
||||
import MonthlySalesChart from "@/components/ecommerce/MonthlySalesChart";
|
||||
import StatisticsChart from "@/components/ecommerce/StatisticsChart";
|
||||
import RecentOrders from "@/components/ecommerce/RecentOrders";
|
||||
import DemographicCard from "@/components/ecommerce/DemographicCard";
|
||||
import Dashboard from "./Dashboard";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title:
|
||||
@@ -13,24 +8,8 @@ export const metadata: Metadata = {
|
||||
description: "한동정보통신 스마트서비스",
|
||||
};
|
||||
|
||||
export default function Dashboard() {
|
||||
export default function DashboardPage() {
|
||||
return (
|
||||
<div className="grid grid-cols-12 gap-4 md:gap-6">
|
||||
<div className="col-span-12 space-y-6 xl:col-span-7">
|
||||
<EcommerceMetrics />
|
||||
|
||||
<MonthlySalesChart />
|
||||
</div>
|
||||
|
||||
<div className="col-span-12 xl:col-span-5">
|
||||
<MonthlyTarget />
|
||||
</div>
|
||||
|
||||
<div className="col-span-12">
|
||||
<StatisticsChart />
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
<Dashboard />
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
"use client"
|
||||
import PageBreadcrumb from "@/components/common/PageBreadCrumb2";
|
||||
import ComponentCard from '@/components/common/ComponentCard2';
|
||||
import { PageIcon } from "@/icons";
|
||||
|
||||
import React from "react";
|
||||
import { COMPANY_NAME, EFFECTIVE_DATE, SERVICE_NAME, TERMS_ARTICLES, TermsArticle } from "./termsData";
|
||||
|
||||
|
||||
const CIRCLED = ["①", "②", "③", "④", "⑤", "⑥", "⑦", "⑧", "⑨", "⑩"];
|
||||
|
||||
const Article = ({ article }: { article: TermsArticle }) => (
|
||||
<section className="border-b border-gray-100 py-5 last:border-0 dark:border-gray-800">
|
||||
<h3 className="mb-3 text-base font-semibold text-gray-800 dark:text-white/90">{article.title}</h3>
|
||||
|
||||
{ article.paragraphs?.map((text, index) => (
|
||||
<p key={index} className="mb-3 text-sm leading-relaxed text-gray-700 dark:text-gray-300">{text}</p>
|
||||
))}
|
||||
|
||||
{ article.items && article.items.length > 0 &&
|
||||
<ol className="space-y-2">
|
||||
{ article.items.map((text, index) => (
|
||||
<li key={index} className="text-sm leading-relaxed text-gray-700 dark:text-gray-300">
|
||||
<span className="mr-1.5 text-gray-500 dark:text-gray-400">{index + 1}.</span>
|
||||
{text}
|
||||
|
||||
{ article.subItems?.[index] &&
|
||||
<ul className="mt-2 space-y-1.5 pl-5">
|
||||
{ article.subItems[index].map((sub, subIndex) => (
|
||||
<li key={subIndex} className="text-sm leading-relaxed text-gray-600 dark:text-gray-400">
|
||||
<span className="mr-1.5">{CIRCLED[subIndex] ?? "-"}</span>
|
||||
{sub}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
}
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
}
|
||||
</section>
|
||||
);
|
||||
|
||||
export default function Terms() {
|
||||
return (
|
||||
<div>
|
||||
<PageBreadcrumb pageTitle1="게시판" pageTitle2="이용약관" />
|
||||
|
||||
<ComponentCard title={SERVICE_NAME + " 이용약관"} titleIcon={<PageIcon />}>
|
||||
<div className="rounded-lg border border-gray-200 bg-gray-50 p-4 text-sm text-gray-600 dark:border-gray-800 dark:bg-white/[0.03] dark:text-gray-400">
|
||||
본 약관은 {COMPANY_NAME}(이하 ‘회사’)가 제공하는 {SERVICE_NAME}의 이용조건과 절차, 회사와 회원의 권리·의무를 정합니다.
|
||||
<br />
|
||||
시행일: {EFFECTIVE_DATE}
|
||||
</div>
|
||||
|
||||
<div className="mt-2">
|
||||
{ TERMS_ARTICLES.map((article, index) => (
|
||||
<Article key={index} article={article} />
|
||||
))}
|
||||
|
||||
<section className="pt-5">
|
||||
<h3 className="mb-3 text-base font-semibold text-gray-800 dark:text-white/90">부칙</h3>
|
||||
<p className="text-sm leading-relaxed text-gray-700 dark:text-gray-300">
|
||||
이 약관은 {EFFECTIVE_DATE}부터 시행합니다.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
</ComponentCard>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import Terms from './Terms';
|
||||
import { Metadata } from "next";
|
||||
import React from "react";
|
||||
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "이용약관",
|
||||
description: "이용약관",
|
||||
};
|
||||
|
||||
export default function TermsPage() {
|
||||
return (
|
||||
<Terms />
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
// 한동스마트서비스 이용약관
|
||||
// 이지샵(www.easyshop.co.kr) 서비스 이용약관의 구성을 참고하여, 무인기기 관리 서비스에 맞게
|
||||
// 조항을 재구성한 것이다. 시행일과 세부 요금/환불 정책은 운영 정책에 맞추어 조정한다.
|
||||
|
||||
export const COMPANY_NAME = "한동정보통신";
|
||||
export const SERVICE_NAME = "한동스마트서비스";
|
||||
export const EFFECTIVE_DATE = "2026년 9월 10일";
|
||||
|
||||
export type TermsArticle = {
|
||||
title: string;
|
||||
paragraphs?: string[]; // 번호 없는 본문
|
||||
items?: string[]; // 1. 2. 3. 형태의 항
|
||||
subItems?: { [index: number]: string[] }; // 해당 항 아래의 ① ② ③ 목
|
||||
};
|
||||
|
||||
export const TERMS_ARTICLES: TermsArticle[] = [
|
||||
{
|
||||
title: "제1조 (목적)",
|
||||
paragraphs: [
|
||||
`이 약관은 ${COMPANY_NAME}(이하 '회사'라 합니다)가 제공하는 ${SERVICE_NAME}(이하 '서비스'라 합니다)를 이용함에 있어 회사와 이용회원(이하 '회원'이라 합니다) 간의 권리·의무 및 책임사항, 기타 필요한 사항을 규정함을 목적으로 합니다.`,
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "제2조 (용어의 정의)",
|
||||
paragraphs: ["이 약관에서 사용하는 용어의 정의는 다음과 같습니다."],
|
||||
items: [
|
||||
"'서비스'라 함은 회사가 웹 및 모바일 환경을 통하여 제공하는 무인기기 원격관리 서비스를 말하며, 무인기기 및 카드단말기 등록·관리, 거래내역 조회, 매출 집계, 상품 및 재고 관리, 기기 이상·품절 내역 조회, 알림 발송 등을 포함합니다.",
|
||||
"'회원'이라 함은 이 약관에 동의하고 회사와 이용계약을 체결하여 서비스를 이용하는 사업자 또는 그 사업자가 지정한 사용자를 말합니다.",
|
||||
"'무인기기'라 함은 회원이 운영하는 자동판매기 등 회사의 서비스에 등록된 기기를 말합니다.",
|
||||
"'단말기'라 함은 무인기기에 연결되어 결제를 처리하는 카드단말기를 말하며, 단말기 고유번호(TID)로 식별합니다.",
|
||||
"'계정'이라 함은 회원의 식별과 서비스 이용을 위하여 회사가 부여한 아이디와 비밀번호의 조합을 말합니다.",
|
||||
"'사업자 그룹'이라 함은 회원이 운영하는 사업장을 상위·하위 단위로 구성한 관리 단위를 말하며, 회원은 자신에게 부여된 그룹 범위 내의 정보만 조회할 수 있습니다.",
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "제3조 (약관의 효력 및 변경)",
|
||||
items: [
|
||||
"이 약관은 서비스를 이용하고자 하는 자가 그 내용에 동의함으로써 효력이 발생합니다.",
|
||||
"회사는 관련 법령을 위배하지 않는 범위에서 이 약관을 개정할 수 있습니다.",
|
||||
"회사가 약관을 개정하는 경우에는 적용일자 및 개정사유를 명시하여 적용일자 15일 전부터 서비스 화면에 공지합니다. 다만 회원에게 불리한 변경인 경우에는 적용일자 30일 전부터 공지하고, 전자우편 또는 문자메시지 등으로 개별 통지합니다.",
|
||||
"회사가 전항의 공지 또는 통지를 하면서 적용일자까지 회원이 거부의사를 표시하지 아니하면 개정에 동의한 것으로 본다는 뜻을 명확히 고지하였음에도 회원이 명시적으로 거부의사를 표시하지 아니한 경우, 회원은 개정약관에 동의한 것으로 봅니다.",
|
||||
"개정약관에 동의하지 아니하는 회원은 이용계약을 해지할 수 있습니다.",
|
||||
"이 약관에 명시되지 아니한 사항은 전자상거래 등에서의 소비자보호에 관한 법률, 약관의 규제에 관한 법률, 정보통신망 이용촉진 및 정보보호 등에 관한 법률 등 관련 법령 또는 상관례에 따릅니다.",
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "제4조 (이용계약의 성립 및 승낙의 제한)",
|
||||
items: [
|
||||
"이용계약은 서비스를 이용하고자 하는 자가 이 약관에 동의하고 회사가 정한 절차에 따라 이용을 신청한 후, 회사가 이를 승낙함으로써 성립합니다.",
|
||||
"회사는 다음 각 호에 해당하는 신청에 대하여는 승낙을 하지 아니하거나 사후에 이용계약을 해지할 수 있습니다.",
|
||||
"회원은 이용 신청 시 회사가 요구하는 사업자 정보와 기기 정보를 사실과 일치하도록 기재하여야 하며, 기재사항이 변경된 경우 지체 없이 이를 갱신하여야 합니다.",
|
||||
"회원이 정보를 사실과 다르게 기재함으로써 발생한 불이익에 대하여 회사는 책임을 지지 아니합니다.",
|
||||
],
|
||||
subItems: {
|
||||
1: [
|
||||
"타인의 명의 또는 사업자 정보를 도용하거나 허위로 기재한 경우",
|
||||
"이용 신청자가 이 약관에 따라 이전에 이용계약이 해지된 사실이 있는 경우",
|
||||
"서비스 이용 요금을 정당한 사유 없이 납부하지 아니한 경우",
|
||||
"회사의 설비 여유가 없거나 기술상 지장이 있는 경우",
|
||||
"기타 회사가 정한 이용 신청 요건을 충족하지 못한 경우",
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "제5조 (서비스의 내용)",
|
||||
items: [
|
||||
"회사는 회원에게 다음 각 호의 서비스를 제공합니다.",
|
||||
"회사는 서비스의 품질 향상, 운영상·기술상의 필요에 따라 제공하는 서비스의 내용을 변경하거나 새로운 서비스를 추가할 수 있으며, 이 경우 변경 내용과 적용일자를 사전에 공지합니다.",
|
||||
],
|
||||
subItems: {
|
||||
0: [
|
||||
"무인기기 및 카드단말기 등록·조회·관리",
|
||||
"거래내역 조회 및 영수증 확인",
|
||||
"일별·기간별 매출 집계 및 통계 조회",
|
||||
"상품 정보, 컬럼별 상품 배치 및 재고 관리",
|
||||
"무인기기 이상(ERROR) 및 품절 내역 조회",
|
||||
"장애·품절 알림 등 문자메시지 발송",
|
||||
"사업자 그룹 및 사용자 계정 관리",
|
||||
"기타 회사가 정하는 부가 서비스",
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "제6조 (서비스의 제공 시간 및 중단)",
|
||||
items: [
|
||||
"서비스는 연중무휴 1일 24시간 제공함을 원칙으로 합니다.",
|
||||
"회사는 시스템 점검, 설비의 보수·교체, 서비스 개선 등의 사유가 있는 경우 서비스의 전부 또는 일부를 일시 중단할 수 있으며, 이 경우 중단 사유와 기간을 사전에 공지합니다. 다만 긴급한 사유가 있는 경우에는 사후에 공지할 수 있습니다.",
|
||||
"천재지변, 정전, 통신망 장애, 카드사·부가통신사업자 등 제휴사의 시스템 장애 등 회사가 통제할 수 없는 사유로 서비스를 제공할 수 없는 경우 회사는 서비스 제공 의무를 면합니다.",
|
||||
"무인기기 또는 단말기의 통신 상태에 따라 거래내역 및 기기 상태 정보의 수집이 지연될 수 있으며, 이 경우 회사는 정상화를 위하여 필요한 조치를 취합니다.",
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "제7조 (서비스 이용의 제한)",
|
||||
items: [
|
||||
"회사는 회원이 다음 각 호에 해당하는 경우 사전 통지 후 서비스 이용을 제한하거나 이용계약을 해지할 수 있습니다. 다만 긴급을 요하는 경우에는 조치 후 통지할 수 있습니다.",
|
||||
"회사가 전항에 따라 이용을 제한하는 경우, 회원은 그 사유가 발생한 날부터 10일 이내에 서면 또는 전자우편으로 이의를 제기할 수 있으며, 회사는 이를 접수한 날부터 10일 이내에 처리 결과를 회신합니다.",
|
||||
],
|
||||
subItems: {
|
||||
0: [
|
||||
"서비스의 안정적 운영을 방해할 목적으로 대량의 정보를 전송하거나 시스템에 부하를 유발하는 경우",
|
||||
"다른 회원의 계정 또는 비밀번호를 부정하게 사용하는 경우",
|
||||
"서비스를 통하여 취득한 정보를 회사의 사전 승낙 없이 제3자에게 제공하거나 영리 목적으로 이용하는 경우",
|
||||
"서비스를 이용하여 법령에 위반되는 행위를 하거나 이를 교사·방조하는 경우",
|
||||
"이 약관에서 정한 회원의 의무를 위반한 경우",
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "제8조 (유료 서비스 및 이용요금)",
|
||||
items: [
|
||||
"회사가 제공하는 서비스는 무료 서비스와 유료 서비스로 구분되며, 유료 서비스의 종류와 요금은 회사와 회원 간의 개별 계약 또는 서비스 화면에 게시된 내용에 따릅니다.",
|
||||
"문자메시지 발송 등 사용량에 따라 과금되는 서비스의 요금은 실제 사용량을 기준으로 산정합니다.",
|
||||
"회사는 이용요금을 변경하는 경우 제3조에서 정한 방법에 따라 사전에 공지합니다.",
|
||||
"회원이 이용요금을 납부기일까지 납부하지 아니하는 경우 회사는 유료 서비스의 제공을 중단할 수 있습니다.",
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "제9조 (계약의 해지 및 환불)",
|
||||
items: [
|
||||
"회원은 언제든지 회사가 정한 절차에 따라 이용계약의 해지를 신청할 수 있으며, 회사는 관련 법령이 정하는 바에 따라 이를 지체 없이 처리합니다.",
|
||||
"회원이 유료 서비스를 전혀 이용하지 아니한 경우, 결제일부터 7일 이내에 결제 취소를 요청할 수 있습니다.",
|
||||
"이미 제공된 서비스에 해당하는 요금은 환불되지 아니하며, 잔여 기간에 대한 요금은 회사가 정한 기준에 따라 일할 계산하여 환불합니다.",
|
||||
"환불은 회원이 결제한 수단과 동일한 방법으로 처리함을 원칙으로 하며, 동일한 방법으로 환불이 불가능한 경우 회사는 사전에 그 사유를 안내하고 다른 방법으로 환불합니다.",
|
||||
"회원의 약관 위반으로 회사가 이용계약을 해지하는 경우, 회사는 회원에게 발생한 손해에 대한 배상을 청구할 수 있습니다.",
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "제10조 (과오금)",
|
||||
items: [
|
||||
"회사는 이용요금의 결제와 관련하여 과오금이 발생한 경우 결제와 동일한 방법으로 과오금 전액을 환불합니다. 동일한 방법으로 환불이 불가능한 경우에는 사전에 이를 안내합니다.",
|
||||
"회사의 귀책사유로 과오금이 발생한 경우 회사가 그 환불에 소요되는 비용을 부담하며, 회원의 귀책사유로 과오금이 발생한 경우에는 합리적인 범위에서 회원이 부담합니다.",
|
||||
"회사가 회원의 과오금 환불 요청을 거부하는 경우, 회사는 이용요금이 정당하게 부과되었음을 입증할 책임을 부담합니다.",
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "제11조 (공지 및 정보의 제공)",
|
||||
items: [
|
||||
"회사는 서비스 운영과 관련한 공지사항을 서비스 화면에 게시하거나 전자우편, 문자메시지, 푸시 알림 등의 방법으로 회원에게 통지할 수 있습니다.",
|
||||
"회사는 무인기기의 장애·품절 등 운영에 필요한 정보를 회원이 등록한 연락처로 발송할 수 있습니다.",
|
||||
"광고성 정보는 사전에 수신에 동의한 회원에게만 발송하며, 회원은 언제든지 수신 동의를 철회할 수 있습니다.",
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "제12조 (개인정보의 보호)",
|
||||
items: [
|
||||
"회사는 서비스 제공에 필요한 최소한의 범위에서 회원의 개인정보를 수집하며, 수집한 개인정보는 관련 법령과 회사의 개인정보처리방침에 따라 보호·관리합니다.",
|
||||
"회사는 회원의 동의 없이 개인정보를 제3자에게 제공하지 아니합니다. 다만 법령에 따라 요구되는 경우에는 그러하지 아니합니다.",
|
||||
"회원은 자신의 개인정보에 대한 열람·정정·삭제 및 처리정지를 언제든지 요구할 수 있습니다.",
|
||||
"개인정보의 처리에 관한 상세한 사항은 회사의 개인정보처리방침에 따릅니다.",
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "제13조 (회원의 의무)",
|
||||
items: [
|
||||
"회원은 관계 법령, 이 약관, 서비스 이용안내 및 회사가 공지한 사항을 준수하여야 하며, 회사의 업무를 방해하는 행위를 하여서는 아니 됩니다.",
|
||||
"회원은 자신의 계정과 비밀번호를 관리할 책임이 있으며, 이를 제3자에게 이용하게 하여서는 아니 됩니다. 관리 소홀로 발생한 결과에 대한 책임은 회원에게 있습니다.",
|
||||
"회원은 계정 정보가 도용되거나 제3자가 무단으로 사용하고 있음을 인지한 경우 즉시 회사에 통지하고 회사의 안내에 따라야 합니다.",
|
||||
"회원은 서비스를 통하여 조회한 거래내역, 매출정보 등을 서비스 이용 목적 외의 용도로 사용하여서는 아니 됩니다.",
|
||||
"회원은 무인기기 및 단말기 정보를 사실과 일치하도록 등록·관리하여야 하며, 기기의 교체·폐기 등 변경사항을 지체 없이 반영하여야 합니다.",
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "제14조 (회사의 의무)",
|
||||
items: [
|
||||
"회사는 관련 법령과 이 약관을 준수하며, 계속적이고 안정적으로 서비스를 제공하기 위하여 최선을 다합니다.",
|
||||
"회사는 회원의 개인정보 보호를 위한 보안 시스템을 갖추고 개인정보처리방침을 공시하고 준수합니다.",
|
||||
"회사는 서비스 이용과 관련하여 회원이 제기한 의견이나 불만이 정당하다고 인정되는 경우 이를 처리하며, 처리 과정과 결과를 회원에게 안내합니다.",
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "제15조 (손해배상 및 면책)",
|
||||
items: [
|
||||
"회사는 다음 각 호의 사유로 회원 또는 제3자에게 발생한 손해에 대하여 책임을 지지 아니합니다.",
|
||||
"회사는 회원이 서비스를 이용하여 기대하는 수익을 얻지 못하였거나, 서비스를 통하여 조회한 정보를 신뢰함으로써 발생한 손해에 대하여 책임을 지지 아니합니다.",
|
||||
"회사는 회원 상호간 또는 회원과 제3자 간에 발생한 분쟁에 개입할 의무가 없으며, 회사의 귀책사유가 없는 한 이로 인한 손해를 배상할 책임이 없습니다.",
|
||||
"회원이 이 약관을 위반하여 회사에 손해를 입힌 경우, 회원은 회사에 그 손해를 배상하여야 합니다.",
|
||||
],
|
||||
subItems: {
|
||||
0: [
|
||||
"천재지변 또는 이에 준하는 불가항력으로 서비스를 제공할 수 없는 경우",
|
||||
"회원이 계정 또는 비밀번호의 관리를 소홀히 한 경우",
|
||||
"회사의 관리 영역이 아닌 통신망의 장애로 서비스 이용이 불가능한 경우",
|
||||
"무인기기 또는 단말기 자체의 고장·오작동으로 인한 경우",
|
||||
"기타 회사의 귀책사유가 없는 사유로 인한 경우",
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "제16조 (분쟁의 해결)",
|
||||
items: [
|
||||
"회사와 회원은 서비스와 관련하여 분쟁이 발생한 경우 원만한 해결을 위하여 성실히 협의합니다.",
|
||||
"협의가 이루어지지 아니한 경우, 회사의 본점 소재지를 관할하는 법원을 제1심 관할법원으로 합니다.",
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -27,6 +27,49 @@ import PageCountSelector from "@/components/PageCountSelector";
|
||||
import InputSelectField from "@/components/form/input/InputSelectField";
|
||||
|
||||
|
||||
const PAY_TYPE_LABEL: { [key: string]: string } = {
|
||||
D1: "신용승인", D4: "신용취소",
|
||||
I1: "페이승인", I4: "페이취소",
|
||||
B1: "현금",
|
||||
TM1: "티머니승인", TM4: "티머니취소",
|
||||
EB1: "캐시비승인", EB4: "캐시비취소",
|
||||
};
|
||||
|
||||
// A credit cancel carries rtid, so the list also brings down the original approval
|
||||
const isCreditCancel = (item: any) => item.type === "D4" && item.rtid;
|
||||
|
||||
// Only the first 6 digits are stored: pad to 4-4-4-4 and mask the unknown digits
|
||||
const formatCardNum = (card_num: any) => {
|
||||
const digits = String(card_num ?? "").replace(/[^0-9]/g, "");
|
||||
if (digits === "")
|
||||
return "";
|
||||
|
||||
const filled = (digits + "****************").substring(0, 16);
|
||||
return (filled.match(/.{1,4}/g) ?? []).join("-");
|
||||
};
|
||||
|
||||
const toAmount = (amount: any) => (amount === null || amount === undefined ? "" : Number(amount).toLocaleString("ko-KR") + "원");
|
||||
|
||||
const payCardName = (type: string, pay_name: string) => (
|
||||
type === "TM1" || type === "TM4" ? "티머니" :
|
||||
type === "EB1" || type === "EB4" ? "캐시비" :
|
||||
pay_name
|
||||
);
|
||||
|
||||
/** A credit cancel is drawn as two lines, the dim second line being the original approval */
|
||||
const cell = (item: any, main: React.ReactNode, sub?: React.ReactNode) => {
|
||||
if (!isCreditCancel(item))
|
||||
return main;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>{main}</div>
|
||||
<div className="mt-1 text-gray-400 dark:text-gray-500">{sub !== undefined && sub !== null && sub !== "" ? sub : " "}</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
const SEARCH_LABEL_WIDTH_PX = 80;
|
||||
//const COUNT_PER_PAGE = 10;
|
||||
const PAGE_NUM_COUNT = 5;
|
||||
@@ -240,13 +283,22 @@ export default function Transactions() {
|
||||
};
|
||||
|
||||
const handleRowClickReceipt = (row: number) => {
|
||||
const node = findRootNodeByKey(treeData, String(tokenPayload?.biz_group_id));
|
||||
modalData.setAll({...tableData[row],
|
||||
biz_group_name: node.name,
|
||||
biz_reg_num: node.biz_reg_num,
|
||||
ceo: node.ceo,
|
||||
phone: node.phone,
|
||||
address: node.address,
|
||||
// The receipt belongs to the group being listed, not to the signed-in account:
|
||||
// a super admin browses other groups, so its own biz_group_id is not in the tree.
|
||||
const selectedKey = selectedTreeKeys[0];
|
||||
const node = selectedKey != undefined ? findRootNodeByKey(treeData, String(selectedKey)) : null;
|
||||
|
||||
if (node == null) {
|
||||
alert("사업자 정보를 찾을 수 없습니다. 좌측에서 사업자를 선택한 후 다시 시도하세요.");
|
||||
return;
|
||||
}
|
||||
|
||||
modalData.setAll({...tableData[row],
|
||||
biz_group_name: node.name ?? "",
|
||||
biz_reg_num: node.biz_reg_num ?? "",
|
||||
ceo: node.ceo ?? "",
|
||||
phone: node.phone ?? "",
|
||||
address: node.address ?? "",
|
||||
});
|
||||
openModal();
|
||||
};
|
||||
@@ -374,28 +426,30 @@ export default function Transactions() {
|
||||
{
|
||||
title: "거래시간",
|
||||
key: "order_time",
|
||||
renderItem: (item: any, row: number) => convertDateTime6(item.order_time),
|
||||
renderItem: (item: any, row: number) => cell(item, convertDateTime6(item.order_time), convertDateTime6(item.org_order_time)),
|
||||
viewMobile: true,
|
||||
},
|
||||
{
|
||||
title: "무인기기명",
|
||||
key: "device_name",
|
||||
renderItem: (item: any) => cell(item, item.device_name),
|
||||
},
|
||||
{
|
||||
title: "단말기 TID",
|
||||
key: "uid1",
|
||||
renderItem: (item: any) => cell(item, item.uid1),
|
||||
viewMobile: true,
|
||||
},
|
||||
{
|
||||
title: "금액",
|
||||
key: "amount",
|
||||
renderItem: (item : any) => (item.amount.toLocaleString("ko-KR") + "원"),
|
||||
renderItem: (item : any) => cell(item, toAmount(item.amount), toAmount(item.org_amount)),
|
||||
viewMobile: true,
|
||||
},
|
||||
{
|
||||
title: "결제유형",
|
||||
key: "type",
|
||||
renderItem: (item : any) => (
|
||||
renderItem: (item : any) => cell(item,
|
||||
<Badge size="sm" color={
|
||||
item.type === "D1" ? "primary" :
|
||||
item.type === "I1" ? "info" :
|
||||
@@ -404,46 +458,40 @@ export default function Transactions() {
|
||||
item.type === "EB1" ? "success" :
|
||||
"error"
|
||||
}>
|
||||
{item.type === "D1" ? "신용승인" :
|
||||
item.type === "D4" ? "신용취소" :
|
||||
item.type === "I1" ? "페이승인" :
|
||||
item.type === "I4" ? "페이취소" :
|
||||
item.type === "B1" ? "현금" :
|
||||
item.type === "TM1" ? "티머니승인" :
|
||||
item.type === "TM4" ? "티머니취소" :
|
||||
item.type === "EB1" ? "캐시비승인" :
|
||||
item.type === "EB4" ? "캐시비취소" :
|
||||
""}
|
||||
</Badge>
|
||||
|
||||
{PAY_TYPE_LABEL[item.type] ?? ""}
|
||||
</Badge>,
|
||||
PAY_TYPE_LABEL[item.org_type] ?? ""
|
||||
)
|
||||
},
|
||||
{
|
||||
title: "승인번호",
|
||||
key: "approval",
|
||||
renderItem: (item: any) => cell(item, item.approval, item.org_approval),
|
||||
},
|
||||
{
|
||||
title: "카드번호",
|
||||
key: "card_num",
|
||||
renderItem: (item: any) => cell(item, formatCardNum(item.card_num), formatCardNum(item.org_card_num)),
|
||||
},
|
||||
{
|
||||
title: "컬럼번호",
|
||||
key: "column_no",
|
||||
renderItem: (item: any) => cell(item, item.column_no, item.org_column_no),
|
||||
},
|
||||
{
|
||||
title: "상품코드",
|
||||
key: "code",
|
||||
renderItem: (item: any) => cell(item, item.code, item.org_code),
|
||||
},
|
||||
{
|
||||
title: "상품명",
|
||||
key: "goods_name",
|
||||
renderItem: (item: any) => cell(item, item.goods_name, item.org_goods_name),
|
||||
},
|
||||
{
|
||||
title: "결제카드",
|
||||
key: "pay_name",
|
||||
renderItem: (item : any) => (
|
||||
item.type === "TM1" ? "티머니" :
|
||||
item.type === "TM4" ? "티머니" :
|
||||
item.type === "EB1" ? "캐시비" :
|
||||
item.type === "EB4" ? "캐시비" :
|
||||
item.pay_name
|
||||
)
|
||||
renderItem: (item : any) => cell(item, payCardName(item.type, item.pay_name), payCardName(item.org_type, item.org_pay_name))
|
||||
},
|
||||
{
|
||||
title: "영수증",
|
||||
|
||||
@@ -20,6 +20,28 @@ export default function SignInForm() {
|
||||
const [user_pw, setUserPw] = useState("");
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [isChecked, setIsChecked] = useState(false);
|
||||
const [isSendingSms, setIsSendingSms] = useState(false); //TempCode: SMS integration test
|
||||
|
||||
// TempCode: SMS integration test, remove when done
|
||||
const handleSendSmsTest = () => {
|
||||
setIsSendingSms(true);
|
||||
|
||||
api.post('/api/send-sms-test.do')
|
||||
.then((resp) => {
|
||||
console.log(resp);
|
||||
|
||||
const r = resp.data;
|
||||
if (r.code === "0000")
|
||||
alert("문자를 전송했습니다.\n수신번호: " + r.phone + "\n잔여건수: " + r.remainCount);
|
||||
else
|
||||
alert("문자 전송에 실패했습니다.\n코드: " + r.code + "\n메시지: " + r.msg);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
alert("문자 전송 요청에 실패했습니다.");
|
||||
})
|
||||
.finally(() => setIsSendingSms(false));
|
||||
};
|
||||
|
||||
const handleLogin = () => {
|
||||
//router.push("/"); //TestCode
|
||||
@@ -124,6 +146,12 @@ export default function SignInForm() {
|
||||
로그인
|
||||
</Button>
|
||||
</div>
|
||||
{/* TempCode: SMS integration test, remove when done */}
|
||||
<div>
|
||||
<Button className="w-full" size="sm" variant="outline" onClick={handleSendSmsTest} disabled={isSendingSms}>
|
||||
{isSendingSms ? "문자 전송 중..." : "문자전송 테스트"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{/*</form>*/}
|
||||
|
||||
|
||||
@@ -73,6 +73,11 @@ const othersItems: NavItem[] = [
|
||||
name: "무인기기 관리",
|
||||
path: "/device",
|
||||
},
|
||||
{
|
||||
icon: <DocsIcon />,
|
||||
name: "무인기기 ERROR/품절내역",
|
||||
path: "/device-error",
|
||||
},
|
||||
{
|
||||
icon: <PieChartIcon />,
|
||||
name: "매출집계",
|
||||
@@ -94,6 +99,7 @@ const othersItems: NavItem[] = [
|
||||
subItems: [
|
||||
{ name: "공지사항", path: "/notice" },
|
||||
{ name: "FAQ", path: "/faq" },
|
||||
{ name: "이용약관", path: "/terms" },
|
||||
//{ name: "자료실", path: "/resource-board" },
|
||||
//{ name: "VOC", path: "/voc" },
|
||||
],
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
// Labels for device_error.error_type.
|
||||
// The telegram itself (device_error.error_code) is parsed on the server:
|
||||
// see component/DeviceErrorCode.java and /api/get-device-error-detail.do
|
||||
|
||||
export const DEVICE_ERROR_TYPE_FAULT = 1; // 단말기 이상
|
||||
export const DEVICE_ERROR_TYPE_SOLD_OUT = 2; // 품절
|
||||
|
||||
export const deviceErrorTypeLabel = (error_type: number | string | null | undefined) => {
|
||||
const value = Number(error_type);
|
||||
if (value === DEVICE_ERROR_TYPE_FAULT) return "단말기 이상";
|
||||
if (value === DEVICE_ERROR_TYPE_SOLD_OUT) return "품절";
|
||||
return "";
|
||||
};
|
||||
Reference in New Issue
Block a user