개발환경 변경 및 설명 추가

This commit is contained in:
comicgum
2026-09-16 13:35:29 +09:00
parent d1e230eb6a
commit ef277abe70
49 changed files with 1176 additions and 236 deletions
@@ -24,15 +24,16 @@ import PageCountSelector from "@/components/PageCountSelector";
const ACCOUNT_STATE_LABEL: { [key: string]: string } = {
"1": "정상", "2": "탈퇴", "3": "휴면", "4": "정지", "5": "잠김",
"1": "정상", "2": "해지", "3": "휴면", "4": "정지", "5": "잠김", "6": "초기화",
};
const ACCOUNT_STATE_OPTIONS = [
{ label: "정상", value: "1" },
{ label: "탈퇴", value: "2" },
{ label: "해지", value: "2" },
{ label: "휴면", value: "3" },
{ label: "정지", value: "4" },
{ label: "잠김", value: "5" },
{ label: "초기화", value: "6" },
];
const accountStateColor = (state: any) => {
@@ -40,6 +41,7 @@ const accountStateColor = (state: any) => {
if (value === 1) return "success";
if (value === 2) return "light";
if (value === 4 || value === 5) return "error";
if (value === 6) return "info";
return "warning";
};
@@ -215,7 +217,9 @@ export default function Account() {
const handleRowClickModify = (row: number) => {
const node = findNodeByKey(treeData, selectedTreeKeys[0]);
modalData.setAll({...tableData[row],
// The password is never listed, so a modification always starts with an empty one
modalData.setAll({...tableData[row],
user_pw: "",
biz_group_name: node.name,
biz_reg_num: node.biz_reg_num,
isModify: true});
@@ -292,7 +296,9 @@ export default function Account() {
{
title: "해지",
key: "_",
// An already terminated account has nothing left to terminate
renderItem: (item: any, row: number) => (
Number(item.state) === 2 ? "" :
<div>
<TrashBinIcon
className="cursor-pointer hover:fill-error-500 dark:hover:fill-error-500 fill-gray-700 dark:fill-gray-400"
@@ -1,6 +1,9 @@
"use client";
import React, { useState, useRef, useEffect } from "react";
import CtModal from "@/components/CtModal";
import ResetPasswordModal from "@/components/ResetPasswordModal";
import Button from "@/components/ui/button/Button2";
import { useModal } from "@/hooks/useModal";
import Label from '@/components/form/Label2';
import Input from '@/components/form/input/InputField2';
import Select from '@/components/form/Select2';
@@ -41,6 +44,8 @@ export default function AddModifyAccountModal({ multiState, onOk, isOpen, closeM
const tokenPayload = useAuthStore((s) => s.tokenPayload); //Bruce, JWT
const [ user_pw_confirm, setUserPwConfirm ] = useState<string>("");
const { isOpen: isOpenReset, openModal: openResetModal, closeModal: closeResetModal } = useModal();
const handleClickAddOrModify = () => {
@@ -102,6 +107,7 @@ export default function AddModifyAccountModal({ multiState, onOk, isOpen, closeM
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 ? "수정할 계정 정보를 입력하세요." : "새로 등록할 계정 정보를 입력하세요."}
@@ -154,15 +160,24 @@ export default function AddModifyAccountModal({ multiState, onOk, isOpen, closeM
placeholder="선택하세요"
options={[
{ label: "정상", value: "1" },
{ label: "탈퇴", value: "2" },
{ label: "해지", value: "2" },
{ label: "휴면", value: "3" },
{ label: "정지", value: "4" },
{ label: "잠김", value: "5" },
{ label: "초기화", value: "6" },
]}
defaultValue={multiState.values.state} onChange={(e) => multiState.set("state", e.target.value)}
className="dark:bg-dark-900" />
</div>
{ multiState.values.isModify &&
<div>
<Label> </Label>
<Button size="sm" variant="outline" onClick={openResetModal}> </Button>
</div>
}
</CtModal>
<ResetPasswordModal gid={multiState.values.gid} phone={multiState.values.phone} isOpen={isOpenReset} closeModal={closeResetModal} />
</>
// </div>
);
};
@@ -3,6 +3,7 @@ import React, { useState } from "react";
import Label from '@/components/form/Label2';
import Input from '@/components/form/input/InputField2';
import CtModal from "@/components/CtModal";
import Checkbox from "@/components/form/input/Checkbox";
import { useMultiState } from "@/hooks/useMultiState";
import api, { formatBusinessNumber, isValidBusinessNumber } from '@/lib/_AG';
@@ -17,8 +18,23 @@ export interface BizGroupModalProps {
email: string;
ceo: string;
address: string;
issue_account: boolean; // new group only: create an account and text the temporary password
}
// Error codes of an account issued together with a new group
const alertIssueError = (errCode: number) => {
if (errCode == -19)
alert("계정발급을 하려면 올바른 휴대폰번호를 입력해야 합니다.");
else if (errCode == -20)
alert("문자 발송에 실패해 사업자 등록이 취소되었습니다. 잠시 후 다시 시도하세요.");
else if (errCode == -21)
alert("이 사업자번호로 발급된 계정이 이미 있습니다.");
else
return false;
return true;
};
interface AddModifyBizGroupModalProps {
multiState: ReturnType<typeof useMultiState<BizGroupModalProps>>;
onOk: (insertedId: number) => void;
@@ -37,7 +53,18 @@ export default function AddModifyBizGroupModal({ multiState, onOk, isOpen, close
// alert("사업자등록번호를 입력하셔야 합니다.");
// return;
//}
if (multiState.values.biz_reg_num != "" && !isValidBusinessNumber(multiState.values.biz_reg_num)) {
if (!multiState.values.isModify && multiState.values.issue_account) {
// The issued ID is the business number and the message goes to the mobile number
if (multiState.values.biz_reg_num == "" || !isValidBusinessNumber(multiState.values.biz_reg_num)) {
alert("계정발급을 하려면 올바른 사업자번호를 입력해야 합니다.");
return;
}
if (!/^01[016789][0-9]{7,8}$/.test(multiState.values.phone.replace(/[^0-9]/g, ""))) {
alert("계정발급을 하려면 올바른 휴대폰번호를 입력해야 합니다.");
return;
}
}
else if (multiState.values.biz_reg_num != "" && !isValidBusinessNumber(multiState.values.biz_reg_num)) {
if (!confirm("잘못된 형식의 사업자등록번호입니다. 그래도 등록하시겠습니까?"))
return;
}
@@ -67,7 +94,7 @@ export default function AddModifyBizGroupModal({ multiState, onOk, isOpen, close
console.log(resp);
if (resp.data.errCode == 0) {
alert("추가되었습니다.");
alert(multiState.values.issue_account ? "추가되었습니다. 발급된 계정 정보를 문자로 보냈습니다." : "추가되었습니다.");
onOk(resp.data.result.biz_group_id);
}
else if (resp.data.errCode == -11) {
@@ -76,7 +103,7 @@ export default function AddModifyBizGroupModal({ multiState, onOk, isOpen, close
else if (resp.data.errCode == -17) {
alert("잘못된 형식의 사업자번호입니다.");
}
else {
else if (alertIssueError(resp.data.errCode) == false) {
alert("일시적으로 사용할 수 없습니다 : " + resp.data.errCode);
}
})
@@ -108,7 +135,7 @@ export default function AddModifyBizGroupModal({ multiState, onOk, isOpen, close
<Input type="text" value={multiState.values.biz_reg_num} onChange={(e) => handleChangeBizRegNum(e.target.value)} />
</div>
<div>
<Label></Label>
<Label></Label>
<Input type="text" value={multiState.values.phone} onChange={(e) => multiState.set("phone", e.target.value)} />
</div>
<div>
@@ -123,6 +150,12 @@ export default function AddModifyBizGroupModal({ multiState, onOk, isOpen, close
<Label></Label>
<Input type="text" value={multiState.values.address} onChange={(e) => multiState.set("address", e.target.value)} />
</div>
{ !multiState.values.isModify &&
<div>
<Checkbox checked={multiState.values.issue_account} onChange={(value) => multiState.set("issue_account", value)}
label="계정발급 (사업자번호를 아이디로, 임시비밀번호를 휴대폰번호로 문자 발송)" />
</div>
}
</CtModal>
// </div>
);
@@ -145,7 +145,7 @@ export default function BizGroupContent({
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: "" });
const modalData = useMultiState<BizGroupModalProps>({isModify: false, pid: "", name: "", biz_reg_num: "", phone: "", email: "", ceo: "", address: "", issue_account: true });
@@ -419,7 +419,7 @@ export default function BizGroupContent({
<Input type="text" value={formInfo.email} onChange={(e) => handleChangeFormInfo("email", e.target.value)} />
</div>
<div>
<Label></Label>
<Label></Label>
<Input type="text" value={formInfo.phone} onChange={(e) => handleChangeFormInfo("phone", e.target.value)} />
</div>
<div>
@@ -3,8 +3,9 @@ import React, { useState } from "react";
import Label from '@/components/form/Label2';
import Input from '@/components/form/input/InputField2';
import CtModal from "@/components/CtModal";
import Checkbox from "@/components/form/input/Checkbox";
import { useMultiState } from "@/hooks/useMultiState";
import api from '@/lib/_AG';
import api, { isValidBusinessNumber } from '@/lib/_AG';
export interface BizGroupModalProps {
@@ -17,6 +18,7 @@ export interface BizGroupModalProps {
email: string;
ceo: string;
address: string;
issue_account: boolean; // new group only: create an account and text the temporary password
}
interface AddModifyBizGroupModalProps {
@@ -37,7 +39,19 @@ export default function AddModifyBizGroupModal({ multiState, onOk, isOpen, close
// alert("사업자등록번호를 입력하셔야 합니다.");
// return;
//}
if (!multiState.values.isModify && multiState.values.issue_account) {
// The issued ID is the business number and the message goes to the mobile number
if (multiState.values.biz_reg_num == "" || !isValidBusinessNumber(multiState.values.biz_reg_num)) {
alert("계정발급을 하려면 올바른 사업자번호를 입력해야 합니다.");
return;
}
if (!/^01[016789][0-9]{7,8}$/.test(multiState.values.phone.replace(/[^0-9]/g, ""))) {
alert("계정발급을 하려면 올바른 휴대폰번호를 입력해야 합니다.");
return;
}
}
if (multiState.values.isModify) {
api.post('/api/modify-biz-group.do', multiState.values).then((resp) => {
console.log(resp);
@@ -57,9 +71,21 @@ export default function AddModifyBizGroupModal({ multiState, onOk, isOpen, close
console.log(resp);
if (resp.data.errCode == 0) {
alert("추가되었습니다.");
alert(multiState.values.issue_account ? "추가되었습니다. 발급된 계정 정보를 문자로 보냈습니다." : "추가되었습니다.");
onOk();
}
else if (resp.data.errCode == -17) {
alert("잘못된 형식의 사업자번호입니다.");
}
else if (resp.data.errCode == -19) {
alert("계정발급을 하려면 올바른 휴대폰번호를 입력해야 합니다.");
}
else if (resp.data.errCode == -20) {
alert("문자 발송에 실패해 사업자 등록이 취소되었습니다. 잠시 후 다시 시도하세요.");
}
else if (resp.data.errCode == -21) {
alert("이 사업자번호로 발급된 계정이 이미 있습니다.");
}
else {
alert("일시적으로 사용할 수 없습니다 : " + resp.data.errCode);
}
@@ -88,7 +114,7 @@ export default function AddModifyBizGroupModal({ multiState, onOk, isOpen, close
<Input type="text" value={multiState.values.biz_reg_num} onChange={(e) => multiState.set("biz_reg_num", e.target.value)} />
</div>
<div>
<Label></Label>
<Label></Label>
<Input type="text" value={multiState.values.phone} onChange={(e) => multiState.set("phone", e.target.value)} />
</div>
<div>
@@ -103,6 +129,12 @@ export default function AddModifyBizGroupModal({ multiState, onOk, isOpen, close
<Label></Label>
<Input type="text" value={multiState.values.address} onChange={(e) => multiState.set("address", e.target.value)} />
</div>
{ !multiState.values.isModify &&
<div>
<Checkbox checked={multiState.values.issue_account} onChange={(value) => multiState.set("issue_account", value)}
label="계정발급 (사업자번호를 아이디로, 임시비밀번호를 휴대폰번호로 문자 발송)" />
</div>
}
</CtModal>
// </div>
);
@@ -49,7 +49,7 @@ export default function BizGroup() {
// modal
const { isOpen, openModal, closeModal } = useModal();
const modalData = useMultiState<BizGroupModalProps>( {isModify: false, pid: "", name: "", biz_reg_num: "", phone: "", email: "", ceo: "", address: "" });
const modalData = useMultiState<BizGroupModalProps>( {isModify: false, pid: "", name: "", biz_reg_num: "", phone: "", email: "", ceo: "", address: "", issue_account: true });
// function part
@@ -141,6 +141,7 @@ export default function BizGroup() {
email: "",
ceo: "",
address: "",
issue_account: true,
});
}
else {
@@ -212,7 +213,7 @@ export default function BizGroup() {
key: "email",
},
{
title: "전화번호",
title: "휴대폰번호",
key: "phone",
},
{
@@ -272,8 +273,8 @@ export default function BizGroup() {
</WithLabel>
</div>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-3">
<WithLabel label="전화번호" label_width={SEARCH_LABEL_WIDTH_PX}>
<Input type="text" placeholder="조회할 전화번호를 입력하세요." value={phone} onChange={(e) => setPhone(e.target.value)} />
<WithLabel label="휴대폰번호" label_width={SEARCH_LABEL_WIDTH_PX}>
<Input type="text" placeholder="조회할 휴대폰번호를 입력하세요." value={phone} onChange={(e) => setPhone(e.target.value)} />
</WithLabel>
<WithLabel label="주소" label_width={SEARCH_LABEL_WIDTH_PX}>
<Input type="text" placeholder="조회할 주소를 입력하세요." value={address} onChange={(e) => setAddress(e.target.value)} />
@@ -1,6 +1,9 @@
"use client";
import React, { useState, useRef, useEffect } from "react";
import CtModal from "@/components/CtModal";
import ResetPasswordModal from "@/components/ResetPasswordModal";
import Button from "@/components/ui/button/Button2";
import { useModal } from "@/hooks/useModal";
import Label from '@/components/form/Label2';
import Input from '@/components/form/input/InputField2';
import Select from '@/components/form/Select2';
@@ -42,6 +45,8 @@ export default function AddModifyCustomerAccountModal({ multiState, onOk, isOpen
const tokenPayload = useAuthStore((s) => s.tokenPayload); //Bruce, JWT
const [ user_pw_confirm, setUserPwConfirm ] = useState<string>("");
const { isOpen: isOpenReset, openModal: openResetModal, closeModal: closeResetModal } = useModal();
const handleClickAddOrModify = () => {
@@ -105,6 +110,7 @@ export default function AddModifyCustomerAccountModal({ multiState, onOk, isOpen
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 ? "수정할 계정 정보를 입력하세요." : "새로 등록할 계정 정보를 입력하세요."}
@@ -164,7 +170,15 @@ export default function AddModifyCustomerAccountModal({ multiState, onOk, isOpen
defaultValue={multiState.values.state} onChange={(e) => multiState.set("state", e.target.value)}
className="dark:bg-dark-900" />
</div>
{ multiState.values.isModify &&
<div>
<Label> </Label>
<Button size="sm" variant="outline" onClick={openResetModal}> </Button>
</div>
}
</CtModal>
<ResetPasswordModal gid={multiState.values.gid} phone={multiState.values.phone} isOpen={isOpenReset} closeModal={closeResetModal} />
</>
// </div>
);
};
@@ -276,7 +276,8 @@ export default function CustomerAccount() {
};
const handleRowClickModify = (row: number) => {
modalData.setAll({...tableData[row], isModify: true});
// The password is never listed, so a modification always starts with an empty one
modalData.setAll({...tableData[row], user_pw: "", isModify: true});
openModal();
};
@@ -100,6 +100,10 @@ export default function DeviceError() {
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 == -13) {
alert("조회 권한이 없습니다.");
return;
}
if (resp.data.errCode != 0) {
alert("일시적으로 사용할 수 없습니다 : " + resp.data.errCode);
return;
@@ -66,6 +66,29 @@ export default function AddModifyDeviceModal({ multiState, onOk, isOpen, closeMo
alert("일시적으로 사용할 수 없습니다 : " + resp.data.errCode);
}
})
.catch((err) => {
console.error(err);
alert("초기화에 실패했습니다.");
})
}
const handleClearEmpty = () => {
if (confirm("품절표시를 끄시겠습니까?") == false)
return;
const params = {
device_id: multiState.values.device_id
}
api.post('/api/clear-device-empty.do', params).then((resp) => {
if (resp.data.errCode == 0) {
alert("품절표시를 껐습니다.");
onOk();
}
else {
alert("일시적으로 사용할 수 없습니다 : " + resp.data.errCode);
}
})
.catch((err) => console.error(err))
}
@@ -188,6 +211,7 @@ export default function AddModifyDeviceModal({ multiState, onOk, isOpen, closeMo
<div>
<Button size="sm" variant="outline" onClick={handleReboot}> </Button>
<Button size="sm" variant="outline" onClick={handleResetTime}> ()</Button>
<Button size="sm" variant="outline" onClick={handleClearEmpty}> </Button>
{/**
<Button size="sm" variant="outline" onClick={handleTest1}>1</Button>
<Button size="sm" variant="outline" onClick={handleTest2}>2</Button>
@@ -21,6 +21,7 @@ import { useModal } from "@/hooks/useModal";
import AddModifyDeviceModal, { DeviceModalProps } from './AddModifyDeviceModal';
import ManageGoodsModal, { GoodsModalProps } from './ManageGoodsModal';
import ManageTemplateModal, { TemplateModalProps } from './ManageTemplateModal';
import ErrorDetailModal, { ErrorDetailModalProps, DeviceErrorDetailItem } from '../device-error/ErrorDetailModal';
import { useMultiState } from "@/hooks/useMultiState";
import CtTree, { CtTreeNode, expandAndSelectTreeNode, getAllNodeKeys, findParentKeys, findNodeByKey, treeToList, addNodeByKey, deleteNodeByKey, findRootNodeByKey } from "@/components/CtTree";
@@ -93,6 +94,10 @@ export default function Device() {
const { isOpen: isOpenManageTemplate, openModal: openModalManageTemplate, closeModal: closeModalManageTemplate } = useModal();
const modalDataManageTemplate = useMultiState<TemplateModalProps>({ biz_group_id: 0, optGoodsList: [] });
const { isOpen: isOpenSoldOut, openModal: openModalSoldOut, closeModal: closeModalSoldOut } = useModal();
const [ soldOutItems, setSoldOutItems ] = useState<DeviceErrorDetailItem[]>([]);
const modalDataSoldOut = useMultiState<ErrorDetailModalProps>({device_error_id: 0, reg_time: "", device_name: "", uid1: "", uid1_type: 0, type: "", error_type: 0, error_code: "", biz_group_name: ""});
// function part
@@ -547,6 +552,33 @@ export default function Device() {
}, [tableOffset]);
// Opens the same detail popup as the device error screen, for the latest sold out record of the device
const handleClickSoldOut = (item: any) => {
api.post('/api/get-device-error-detail.do', { device_id: item.device_id }).then((resp) => {
if (resp.data.errCode == -13) {
alert("조회 권한이 없습니다.");
return;
}
if (resp.data.errCode != 0) {
alert("일시적으로 사용할 수 없습니다 : " + resp.data.errCode);
return;
}
const info = resp.data.result.result_info;
if (!info) {
alert("품절 내역이 없습니다.");
return;
}
modalDataSoldOut.setAll({...info,
biz_group_name: item.biz_group_name ?? "",
});
setSoldOutItems(resp.data.result.list ?? []);
openModalSoldOut();
})
.catch((err) => console.error(err));
};
// UI part
//
const columns = [
@@ -577,10 +609,14 @@ export default function Device() {
const connectTime = item.connect_time ? new Date(item.connect_time.replace(' ', 'T')) : null;
const disconnectTime = item.disconnect_time ? new Date(item.disconnect_time.replace(' ', 'T')) : null;
let color: "success" | "error" | "info" = "info";
let color: "success" | "error" | "info" | "warning" = "info";
let text = "";
if (connectTime && (!disconnectTime || disconnectTime < connectTime)) {
// Sold out (device.is_empty = 1) wins over the connection state
if (Number(item.is_empty) === 1) {
color = "warning";
text = "품절";
} else if (connectTime && (!disconnectTime || disconnectTime < connectTime)) {
color = "success";
text = "가동중";
} else if (connectTime && disconnectTime && disconnectTime > connectTime) {
@@ -588,6 +624,16 @@ export default function Device() {
text = "장애";
}
if (text === "품절") {
return (
<button type="button" className="cursor-pointer hover:opacity-80" onClick={() => handleClickSoldOut(item)}>
<Badge size="sm" color={color}>
{text}
</Badge>
</button>
);
}
return (
<Badge size="sm" color={color}>
{text}
@@ -596,11 +642,11 @@ export default function Device() {
},
viewMobile: true,
},
{
/*{
title: "가동시간",
key: "connect_time",
renderItem: (item: any, row: number) => convertDateTime3(item.connect_time)
},
},*/
{
title: "장애발생시간",
key: "disconnect_time",
@@ -692,6 +738,7 @@ export default function Device() {
{ label: "전체", value: "" },
{ label: "가동중", value: "1" },
{ label: "장애", value: "4" },
{ label: "품절", value: "5" },
]}
defaultValue={conn_state} onChange={handleChangeConnState}
className="dark:bg-dark-900" />
@@ -753,6 +800,8 @@ export default function Device() {
{/*<ManageGoodsModal multiState={modalDataManageGoods} onOk={handleManageGoodsModalOk} isOpen={isOpenManageGoods} closeModal={closeModalManageGoods} />*/}
<ManageTemplateModal multiState={modalDataManageTemplate} onOk={handleManageTemplateModalOk} isOpen={isOpenManageTemplate} closeModal={closeModalManageTemplate} />
<ErrorDetailModal multiState={modalDataSoldOut} items={soldOutItems} onOk={closeModalSoldOut} isOpen={isOpenSoldOut} closeModal={closeModalSoldOut} />
</div>
</div>
);
@@ -107,7 +107,7 @@ export default function AddModifyNoticeModal({ isOpen, openModal, closeModal, mu
<Label></Label>
<select value={multiState.values.state} onChange={(e) => multiState.set("state", e.target.value)}>
<option value={1}></option>
<option value={2}></option>
<option value={2}></option>
<option value={3}></option>
<option value={4}></option>
</select>
@@ -1,70 +1,20 @@
"use client"
import PageBreadcrumb from "@/components/common/PageBreadCrumb2";
import ComponentCard from '@/components/common/ComponentCard2';
import TermsContent from "@/components/TermsContent";
import { PageIcon } from "@/icons";
import React from "react";
import { COMPANY_NAME, EFFECTIVE_DATE, SERVICE_NAME, TERMS_ARTICLES, TermsArticle } from "./termsData";
import { SERVICE_NAME } 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}( &lsquo;&rsquo;) {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>
<TermsContent />
</ComponentCard>
</div>
);
@@ -13,7 +13,11 @@ export interface ReceiptModalProps {
goods_name: string;
amount: number;
pay_name: string;
card_num: string;
approval: string;
type: string;
org_order_time: string;
org_goods_name: string;
biz_group_name: string;
biz_reg_num: string;
ceo: string;
@@ -30,8 +34,15 @@ interface ViewReceiptModalProps {
}
export default function ReceiptModal({ multiState, onOk, isOpen, closeModal }: ViewReceiptModalProps) {
const tax = multiState.values.amount / 10;
const price = multiState.values.amount - tax;
// A credit cancel (D4) prints as a cancel receipt; its approval number is the original one
const isCancel = multiState.values.type === "D4";
const amount = Math.abs(Number(multiState.values.amount) || 0);
const sign = isCancel ? "-" : "";
// A cancel row is stored without goods, so fall back to the original approval's goods name
const goodsName = multiState.values.goods_name || (isCancel ? multiState.values.org_goods_name : "");
const tax = amount / 10;
const price = amount - tax;
const elementRef = useRef<HTMLDivElement>(null);
@@ -61,14 +72,22 @@ export default function ReceiptModal({ multiState, onOk, isOpen, closeModal }: V
<div className="p-5" ref={elementRef}>
<div className="receipt w-[80mm] bg-white text-gray-900 px-5 py-5 mx-auto border border-gray-300 shadow-md text-[13px] leading-relaxed">
<h2 className="text-center text-lg font-bold mb-1">[]</h2>
<h2 className="text-center text-lg font-bold mb-1">{isCancel ? "[취소 영수증]" : "[영수증]"}</h2>
<div className="text-center text-[11px] mb-5">
<div className="text-left text-[11px] mb-5">
: {multiState.values.biz_group_name}<br />
: {multiState.values.address}<br />
: {multiState.values.biz_reg_num} | : {multiState.values.ceo}<br />
: {multiState.values.biz_reg_num}<br />
: {multiState.values.ceo}<br />
TEL: {multiState.values.phone}<br />
{convertDateTime6(multiState.values.order_time)}
{isCancel ?
<>
: {convertDateTime6(multiState.values.order_time)}<br />
: {convertDateTime6(multiState.values.org_order_time)}
</>
:
<>: {convertDateTime6(multiState.values.order_time)}</>
}
</div>
<div className="border-t border-dashed border-black my-2.5" />
@@ -83,9 +102,9 @@ export default function ReceiptModal({ multiState, onOk, isOpen, closeModal }: V
</thead>
<tbody>
<tr>
<td className="py-1">{multiState.values.goods_name ? multiState.values.goods_name : "물품"}</td>
<td className="py-1">{goodsName ? goodsName : "물품"}</td>
<td className="py-1 text-center">1</td>
<td className="py-1 text-right"> {multiState.values.amount.toLocaleString("ko-KR")}</td>
<td className="py-1 text-right">{sign} {amount.toLocaleString("ko-KR")}</td>
</tr>
</tbody>
</table>
@@ -96,15 +115,15 @@ export default function ReceiptModal({ multiState, onOk, isOpen, closeModal }: V
<tbody>
<tr>
<td className="py-1"> ( 1)</td>
<td className="py-1 text-right text-[15px] font-bold border-t-2 border-b-2 border-black"> {price.toLocaleString("ko-KR")}</td>
<td className="py-1 text-right text-[15px] font-bold">{sign} {price.toLocaleString("ko-KR")}</td>
</tr>
<tr>
<td className="py-1">(10%)</td>
<td className="py-1 text-right"> {tax}</td>
<td className="py-1 text-right">{sign} {tax.toLocaleString("ko-KR")}</td>
</tr>
<tr>
<td className="py-1 text-[15px] font-bold border-t-2 border-b-2 border-black"> </td>
<td className="py-1 text-right text-[15px] font-bold border-t-2 border-b-2 border-black"> {multiState.values.amount.toLocaleString("ko-KR")}</td>
<td className="py-1 text-[15px] font-bold border-t-2 border-b-2 border-black">{isCancel ? "최종 취소금액" : "최종 결제금액"}</td>
<td className="py-1 text-right text-[15px] font-bold border-t-2 border-b-2 border-black">{sign} {amount.toLocaleString("ko-KR")}</td>
</tr>
</tbody>
</table>
@@ -117,16 +136,16 @@ export default function ReceiptModal({ multiState, onOk, isOpen, closeModal }: V
<td className="py-1">결제수단: 신용카드 ()</td>
<td className="py-1 text-right"></td>
</tr>
{/*<tr>
<td className="py-1">카드번호: 1234-****-****-1234</td>
<tr>
<td className="py-1">: {multiState.values.card_num}</td>
<td className="py-1 text-right"></td>
</tr>*/}
</tr>
<tr>
<td className="py-1">: {multiState.values.pay_name}</td>
<td className="py-1 text-right"></td>
</tr>
<tr>
<td className="py-1">: {multiState.values.approval}</td>
<td className="py-1">{isCancel ? "원승인번호" : "승인번호"}: {multiState.values.approval}</td>
<td className="py-1 text-right"></td>
</tr>
</tbody>
@@ -56,19 +56,6 @@ const payCardName = (type: string, pay_name: string) => (
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;
@@ -111,7 +98,7 @@ export default function Transactions() {
const [ date_end, setDateEnd ] = useState<string>(todayYMD());
const { isOpen, openModal, closeModal } = useModal();
const modalData = useMultiState<ReceiptModalProps>({order_time: "", goods_name: "", amount: 0, pay_name: "", approval: "", biz_group_name: "", biz_reg_num: "", ceo: "", phone: "", address: ""});
const modalData = useMultiState<ReceiptModalProps>({order_time: "", goods_name: "", amount: 0, pay_name: "", card_num: "", approval: "", type: "", org_order_time: "", org_goods_name: "", biz_group_name: "", biz_reg_num: "", ceo: "", phone: "", address: ""});
const [ isMobileMode, setIsMobileMode ] = useState<boolean>(false);
const [ isSearchFieldOpen, setIsSearchFieldOpen ] = useState<boolean>(false);
@@ -294,6 +281,7 @@ export default function Transactions() {
}
modalData.setAll({...tableData[row],
card_num: formatCardNum(tableData[row].card_num),
biz_group_name: node.name ?? "",
biz_reg_num: node.biz_reg_num ?? "",
ceo: node.ceo ?? "",
@@ -426,30 +414,30 @@ export default function Transactions() {
{
title: "거래시간",
key: "order_time",
renderItem: (item: any, row: number) => cell(item, convertDateTime6(item.order_time), convertDateTime6(item.org_order_time)),
renderItem: (item: any, row: number) => convertDateTime6(item.order_time),
viewMobile: true,
},
{
title: "무인기기명",
key: "device_name",
renderItem: (item: any) => cell(item, item.device_name),
renderItem: (item: any) => item.device_name,
},
{
title: "단말기 TID",
key: "uid1",
renderItem: (item: any) => cell(item, item.uid1),
renderItem: (item: any) => item.uid1,
viewMobile: true,
},
{
title: "금액",
key: "amount",
renderItem: (item : any) => cell(item, toAmount(item.amount), toAmount(item.org_amount)),
renderItem: (item : any) => toAmount(item.amount),
viewMobile: true,
},
{
title: "결제유형",
key: "type",
renderItem: (item : any) => cell(item,
renderItem: (item : any) => (
<Badge size="sm" color={
item.type === "D1" ? "primary" :
item.type === "I1" ? "info" :
@@ -459,47 +447,46 @@ export default function Transactions() {
"error"
}>
{PAY_TYPE_LABEL[item.type] ?? ""}
</Badge>,
PAY_TYPE_LABEL[item.org_type] ?? ""
</Badge>
)
},
{
title: "승인번호",
key: "approval",
renderItem: (item: any) => cell(item, item.approval, item.org_approval),
renderItem: (item: any) => item.approval,
},
{
title: "카드번호",
key: "card_num",
renderItem: (item: any) => cell(item, formatCardNum(item.card_num), formatCardNum(item.org_card_num)),
renderItem: (item: any) => formatCardNum(item.card_num),
},
{
title: "컬럼번호",
key: "column_no",
renderItem: (item: any) => cell(item, item.column_no, item.org_column_no),
renderItem: (item: any) => item.column_no,
},
{
title: "상품코드",
key: "code",
renderItem: (item: any) => cell(item, item.code, item.org_code),
renderItem: (item: any) => item.code,
},
{
title: "상품명",
key: "goods_name",
renderItem: (item: any) => cell(item, item.goods_name, item.org_goods_name),
renderItem: (item: any) => item.goods_name,
},
{
title: "결제카드",
key: "pay_name",
renderItem: (item : any) => cell(item, payCardName(item.type, item.pay_name), payCardName(item.org_type, item.org_pay_name))
renderItem: (item : any) => payCardName(item.type, item.pay_name)
},
{
title: "영수증",
key: "_",
renderItem: (item: any, row: number) => (
item.type == "D1" ?
item.type == "D1" || item.type == "D4" ?
<div>
<DocsIcon
<DocsIcon
className="cursor-pointer hover:fill-error-500 dark:hover:fill-error-500 fill-gray-700 dark:fill-gray-400"
onClick={() => {handleRowClickReceipt(row)}} />
</div>
@@ -524,6 +511,11 @@ export default function Transactions() {
onClick={() => {handleRowClickDelete(row)}}>
</button>
: isCreditCancel(item) ?
<div className="text-theme-xs font-medium text-blue-light-700 dark:text-blue-light-400">
<div>[]</div>
<div>{convertDateTime6(item.org_order_time)}</div>
</div>
: ""
),
},
@@ -0,0 +1,11 @@
import ChangeInfoForm from "@/components/auth/ChangeInfoForm";
import { Metadata } from "next";
export const metadata: Metadata = {
title: "계정정보 변경",
description: "계정정보 변경",
};
export default function ChangeInfo() {
return <ChangeInfoForm />;
}
@@ -0,0 +1,65 @@
"use client";
import React, { useEffect, useState } from "react";
import CtModal from "@/components/CtModal";
import Label from '@/components/form/Label2';
import Input from '@/components/form/input/InputField2';
import api from '@/lib/_AG';
interface ResetPasswordModalProps {
gid: string;
phone: string; // the account's mobile number, shown as the default
isOpen: boolean;
closeModal: () => void;
}
/** Sends a new temporary password to the account holder's mobile number */
export default function ResetPasswordModal({ gid, phone, isOpen, closeModal }: ResetPasswordModalProps) {
const [ inputPhone, setInputPhone ] = useState<string>("");
useEffect(() => {
if (isOpen)
setInputPhone(phone ?? "");
}, [isOpen, phone]);
const handleClickSend = () => {
if (inputPhone.trim() == "") {
alert("휴대폰 번호를 입력하세요");
return;
}
api.post('/api/reset-account-password.do', { gid: gid, phone: inputPhone }).then((resp) => {
if (resp.data.errCode == 0) {
alert("임시 비밀번호 전송완료");
closeModal();
}
else {
alert("전송에 실패했습니다.");
}
})
.catch((err) => {
console.error(err);
alert("전송에 실패했습니다.");
});
};
return (
<CtModal
title="비밀번호 초기화"
okBtnText="임시 비밀번호 전송"
onClickOk={handleClickSend}
isOpen={isOpen}
closeModal={closeModal}
className="max-w-[500px] p-6 lg:p-10"
>
<div className="whitespace-pre-line text-sm text-gray-700 dark:text-gray-400">
{"비밀번호 초기화는 사용자의 휴대폰에 임시 비밀번호를 문자로 전송합니다.\n사용자의 휴대폰 번호를 입력하세요."}
</div>
<div>
<Label> </Label>
<Input type="text" placeholder="휴대폰 번호를 입력하세요." value={inputPhone} onChange={(e) => setInputPhone(e.target.value)}
onKeyDown={(e) => {if (e.key === "Enter") handleClickSend()}} />
</div>
</CtModal>
);
};
@@ -0,0 +1,64 @@
"use client";
import React from "react";
import { COMPANY_NAME, EFFECTIVE_DATE, SERVICE_NAME, TERMS_ARTICLES, TermsArticle } from "@/app/(admin)/terms/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>
);
/** The terms of service text, shared by the terms page and the account change page */
export default function TermsContent() {
return (
<>
<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}( &lsquo;&rsquo;) {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>
</>
);
}
@@ -0,0 +1,167 @@
"use client";
import Input from "@/components/form/input/InputField2";
import Label from "@/components/form/Label";
import Button from "@/components/ui/button/Button";
import { EyeCloseIcon, EyeIcon } from "@/icons";
import React, { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import Checkbox from "@/components/form/input/Checkbox";
import TermsContent from "@/components/TermsContent";
import api, { useInitAccountStore } from '@/lib/_AG';
/** Replaces the temporary ID and password of an issued account, then sends the user back to sign in */
export default function ChangeInfoForm() {
const router = useRouter();
const tempId = useInitAccountStore((s) => s.user_id);
const tempPw = useInitAccountStore((s) => s.user_pw);
const [new_id, setNewId] = useState("");
const [new_pw, setNewPw] = useState("");
const [new_pw_confirm, setNewPwConfirm] = useState("");
const [showPassword, setShowPassword] = useState(false);
const [isAgreed, setIsAgreed] = useState(false);
// Only reachable right after signing in with a temporary password
useEffect(() => {
if (tempId == "" || tempPw == "")
router.replace("/signin");
}, [tempId, tempPw, router]);
const handleChange = () => {
if (new_id == "") {
alert("아이디를 입력하셔야 합니다.");
return;
}
if (!/^[A-Za-z0-9._-]{4,50}$/.test(new_id)) {
alert("아이디는 영문, 숫자, . _ - 로 4자 이상 입력해야 합니다.");
return;
}
if (new_pw == "") {
alert("비밀번호를 입력하셔야 합니다.");
return;
}
if (new_pw.length < 8 || !/[A-Za-z]/.test(new_pw) || !/[0-9]/.test(new_pw)) {
alert("비밀번호는 영문과 숫자를 포함해 8자 이상 입력해야 합니다.");
return;
}
if (new_pw_confirm == "") {
alert("비밀번호 확인을 입력하셔야 합니다.");
return;
}
if (new_pw !== new_pw_confirm) {
alert("비밀번호 확인이 일치하지 않습니다.");
return;
}
if (!isAgreed) {
alert("이용약관에 동의해야 서비스를 이용할 수 있습니다.");
return;
}
const params = {
id: tempId,
pw: tempPw,
new_id: new_id,
new_pw: new_pw,
agree: true,
};
api.post('/api/change-init-account.do', params).then((resp) => {
if (resp.data.errCode == 0) {
useInitAccountStore.getState().clearInitAccount();
alert("정보가 변경되었습니다. 변경된 아이디와 비밀번호로 다시 로그인하세요.");
router.replace("/signin");
}
else if (resp.data.errCode == -21) {
alert("이미 사용 중인 아이디입니다.");
}
else if (resp.data.errCode == -22) {
alert("아이디 또는 비밀번호 형식이 올바르지 않습니다. 임시비밀번호와 같은 비밀번호는 사용할 수 없습니다.");
}
else if (resp.data.errCode == -9) {
useInitAccountStore.getState().clearInitAccount();
alert("계정 정보를 확인할 수 없습니다. 다시 로그인하세요.");
router.replace("/signin");
}
else {
alert("일시적으로 사용할 수 없습니다 : " + resp.data.errCode);
}
})
.catch((err) => console.error(err));
};
return (
<div className="flex flex-col flex-1 lg:w-1/2 w-full">
<div className="flex flex-col justify-center flex-1 w-full max-w-xl mx-auto">
<div>
<div className="mb-5 sm:mb-8">
<h1 className="mb-2 font-semibold text-gray-800 text-title-sm dark:text-white/90 sm:text-title-md">
</h1>
<p className="text-sm text-gray-500 dark:text-gray-400">
. .
</p>
</div>
<div className="space-y-6">
<div>
<Label>
<span className="text-error-500">*</span>
</Label>
<Input placeholder="사용할 아이디를 입력하세요" type="text" value={new_id} onChange={(e) => setNewId(e.target.value)} />
</div>
<div>
<Label>
<span className="text-error-500">*</span>
</Label>
<div className="relative">
<Input
type={showPassword ? "text" : "password"}
placeholder="영문, 숫자 포함 8자 이상"
value={new_pw} onChange={(e) => setNewPw(e.target.value)}
/>
<span
onClick={() => setShowPassword(!showPassword)}
className="absolute z-30 -translate-y-1/2 cursor-pointer right-4 top-1/2"
>
{showPassword ? (
<EyeIcon className="fill-gray-500 dark:fill-gray-400" />
) : (
<EyeCloseIcon className="fill-gray-500 dark:fill-gray-400" />
)}
</span>
</div>
</div>
<div>
<Label>
<span className="text-error-500">*</span>
</Label>
<Input
type={showPassword ? "text" : "password"}
placeholder="비밀번호를 다시 입력하세요"
value={new_pw_confirm} onChange={(e) => setNewPwConfirm(e.target.value)}
onKeyDown={(e) => {if (e.key === "Enter") handleChange()}}
/>
</div>
<div>
<Label>
<span className="text-error-500">*</span>
</Label>
<div className="max-h-[220px] overflow-y-auto custom-scrollbar rounded-lg border border-gray-200 p-4 dark:border-gray-800">
<TermsContent />
</div>
<div className="mt-3">
<Checkbox checked={isAgreed} onChange={setIsAgreed} label="이용약관에 동의합니다." />
</div>
</div>
<div>
<Button className="w-full" size="sm" onClick={handleChange}>
</Button>
</div>
</div>
</div>
</div>
</div>
);
}
@@ -9,7 +9,7 @@ import { ChevronLeftIcon, EyeCloseIcon, EyeIcon } from "@/icons";
import Link from "next/link";
import React, { useState } from "react";
import { useRouter } from "next/navigation";
import api, { useAuthStore } from '@/lib/_AG';
import api, { useAuthStore, useInitAccountStore } from '@/lib/_AG';
export default function SignInForm() {
@@ -20,28 +20,6 @@ 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
@@ -63,11 +41,16 @@ export default function SignInForm() {
api.post('/api/sign-in.do', params).then((resp) => {
console.log(resp);
if (resp.data.errCode == 0) {
useAuthStore.getState().setAccessToken(resp.data.result.sval1);
router.push("/");
}
else if (resp.data.errCode == -18) {
// Signed in with the temporary password of an issued account: the ID and password are changed first
useInitAccountStore.getState().setInitAccount(user_id, user_pw);
router.push("/change-info");
}
else if (resp.data.errCode == -9) {
alert("유효하지 않은 아이디 또는 암호입니다.");
}
@@ -146,12 +129,6 @@ 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>*/}
+16
View File
@@ -66,6 +66,22 @@ export const useAuthStore = create<AuthState>((set: any) => ({
setAuthStatus: (status: AuthStatus) => set({ authStatus: status }),
}));
// The temporary ID and password typed on the sign in page, handed to the account change page.
// Kept in memory only: a reload loses it and sends the user back to sign in.
type InitAccountState = {
user_id: string;
user_pw: string;
setInitAccount: (user_id: string, user_pw: string) => void;
clearInitAccount: () => void;
};
export const useInitAccountStore = create<InitAccountState>((set) => ({
user_id: "",
user_pw: "",
setInitAccount: (user_id: string, user_pw: string) => set({ user_id, user_pw }),
clearInitAccount: () => set({ user_id: "", user_pw: "" }),
}));
type UiLoadingState = {
pendingCount: number;
treePendingCount: number;