Compare commits
1 Commits
tyou-impro
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 20eb3a0e81 |
237
multisender_sample.html
Normal file
237
multisender_sample.html
Normal file
@@ -0,0 +1,237 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>MULTI SENDER</title>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no">
|
||||
<style>
|
||||
:root {
|
||||
--gap: 20px;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
margin: 0;
|
||||
font-family: sans-serif;
|
||||
font-size: 12px;
|
||||
scroll-snap-type: y mandatory;
|
||||
scroll-behavior: smooth;
|
||||
scroll-padding-top: 0;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
|
||||
html, body {
|
||||
margin: 0;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
.wrap {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: start;
|
||||
padding: var(--gap);
|
||||
gap: var(--gap);
|
||||
}
|
||||
|
||||
.wrap fieldset {
|
||||
flex-grow: 1;
|
||||
padding: 1.5rem;
|
||||
border: 1px solid #CCC;
|
||||
border-radius: 0.5rem;
|
||||
height: calc(100vh - 80px);
|
||||
}
|
||||
|
||||
.wrap fieldset:first-child {
|
||||
flex-grow: 0;
|
||||
flex-basis: 500px;
|
||||
}
|
||||
|
||||
.wrap fieldset legend {
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
padding: 0 2rem 0 .75rem;
|
||||
}
|
||||
|
||||
|
||||
fieldset ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
fieldset ul > li {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
fieldset label {
|
||||
font-size: 1.2rem;
|
||||
font-weight: bold;
|
||||
margin-bottom: 0.5rem;
|
||||
padding-left: 0.375rem;
|
||||
}
|
||||
|
||||
fieldset label+input {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 3rem;
|
||||
padding: .375rem .75rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 400;
|
||||
line-height: 1.5;
|
||||
color: #495057;
|
||||
background-color: #fff;
|
||||
background-clip: padding-box;
|
||||
border: 1px solid #ced4da;
|
||||
border-radius: .5rem;
|
||||
transition: border-color .15s ease-in-out, box-shadow .15s ease-in-out;
|
||||
}
|
||||
|
||||
fieldset label+input:focus {
|
||||
color: #495057;
|
||||
background-color: #fff;
|
||||
border-color: #808080;
|
||||
outline: 0;
|
||||
box-shadow: 0 0 0 .2rem rgba(100, 100, 100, .25);
|
||||
}
|
||||
|
||||
fieldset label+input[type="file"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
fieldset input[type="file"]+label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 10rem;
|
||||
border: 2px dotted #CCC;
|
||||
border-radius: .5rem;
|
||||
}
|
||||
|
||||
fieldset input[type="file"]+label::before {
|
||||
content: '업로드 할 파일을 선택하거나 드레그&드롭하세요.';
|
||||
color: rgba(0,0,0,.5);
|
||||
font-weight: 400;
|
||||
text-shadow: 0 0 2px rgba(0,0,0,.7);
|
||||
}
|
||||
|
||||
fieldset table {
|
||||
display: table;
|
||||
width: 100%;
|
||||
caption-side: bottom;
|
||||
border-collapse: separate;
|
||||
box-sizing: border-box;
|
||||
border-spacing: 1px;
|
||||
border-color: gray;
|
||||
background-color: #aaa;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
fieldset thead {
|
||||
background-color: rgba(0,0,0,.7);
|
||||
border-bottom: 2px solid rgba(0,0,0,1);
|
||||
color: white;
|
||||
}
|
||||
fieldset th {
|
||||
text-align: center;
|
||||
padding: .375rem .725rem;
|
||||
}
|
||||
fieldset td {
|
||||
background-color: rgba(255,255,255,.9);
|
||||
padding: .2rem .5rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
fieldset tr[row-type="sum"] {
|
||||
border-top: 2px solid #aaa;
|
||||
}
|
||||
|
||||
[data-type="number"] {
|
||||
text-align: right;
|
||||
padding: .2rem 1rem;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1200px) {
|
||||
|
||||
.wrap {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.wrap fieldset {
|
||||
flex-grow: 1 !important;
|
||||
flex-basis: unset;
|
||||
width: 100%;
|
||||
height: unset;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
|
||||
<fieldset>
|
||||
<legend>UPLOAD</legend>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<label for="contract">토큰 컨트랙트</label>
|
||||
<input type="text" id="contract" value="0x36b8dE7c6B06B3f170003452114f0B8E6BcFEE18" />
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<label for="file">보내는 계좌</label>
|
||||
<input type="file" id="file" accept=".xls,.xlsx,.csv" />
|
||||
<label for="file"></label>
|
||||
</li>
|
||||
</ul>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend>DETAIL</legend>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>항목1</th>
|
||||
<th>항목2</th>
|
||||
<th>항목3</th>
|
||||
<th>항목4</th>
|
||||
<th>항목5</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>값1</td>
|
||||
<td>값2</td>
|
||||
<td>값3</td>
|
||||
<td>값4</td>
|
||||
<td data-type="number">1.25</td>
|
||||
</tr>
|
||||
|
||||
<tr row-type="sum">
|
||||
<td colspan="4">항목5 합계</td>
|
||||
<td data-type="number">1.25</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</fieldset>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<script>
|
||||
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
3
multisender_sample.html:Zone.Identifier
Normal file
3
multisender_sample.html:Zone.Identifier
Normal file
@@ -0,0 +1,3 @@
|
||||
[ZoneTransfer]
|
||||
ZoneId=3
|
||||
HostUrl=https://files.slack.com/files-pri/T08K8CRTBD0-F09RSV6BGPK/download/multisender_sample.html?origin_team=T08K8CRTBD0
|
||||
152
src/App.css
152
src/App.css
@@ -36,155 +36,3 @@
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* Status display styles */
|
||||
.status-container {
|
||||
margin-top: 30px;
|
||||
width: 80%;
|
||||
margin: 30px auto 0;
|
||||
}
|
||||
|
||||
.status-header {
|
||||
margin-bottom: 10px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.status-list {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid #ccc;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.status-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 8px;
|
||||
margin: 5px 0;
|
||||
border-radius: 4px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.status-item:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.status-address {
|
||||
font-size: 14px;
|
||||
font-family: monospace;
|
||||
word-break: break-all;
|
||||
max-width: 60%;
|
||||
}
|
||||
|
||||
.status-details {
|
||||
text-align: right;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.status-amount {
|
||||
font-size: 12px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.status-text {
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.status-error {
|
||||
font-size: 10px;
|
||||
margin-top: 2px;
|
||||
color: #721c24;
|
||||
max-width: 200px;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
/* Status colors */
|
||||
.status-pending {
|
||||
background-color: #f8f9fa;
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.status-pending .status-amount,
|
||||
.status-pending .status-text {
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.status-success {
|
||||
background-color: #d4edda;
|
||||
border: 1px solid #c3e6cb;
|
||||
}
|
||||
|
||||
.status-success .status-amount,
|
||||
.status-success .status-text {
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.status-failed {
|
||||
background-color: #f8d7da;
|
||||
border: 1px solid #f5c6cb;
|
||||
}
|
||||
|
||||
.status-failed .status-amount,
|
||||
.status-failed .status-text {
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
/* Error message styles */
|
||||
.error-container {
|
||||
margin-top: 20px;
|
||||
padding: 10px;
|
||||
background-color: #f8d7da;
|
||||
color: #721c24;
|
||||
border: 1px solid #f5c6cb;
|
||||
border-radius: 4px;
|
||||
width: 80%;
|
||||
margin: 20px auto 0;
|
||||
}
|
||||
|
||||
.error-title {
|
||||
font-weight: bold;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
/* Upload area improvements */
|
||||
.upload-area {
|
||||
border: 2px dashed gray;
|
||||
padding: 50px;
|
||||
text-align: center;
|
||||
margin: 50px auto 20px;
|
||||
width: 200px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.upload-area:hover {
|
||||
border-color: #007bff;
|
||||
background-color: #f8f9ff;
|
||||
}
|
||||
|
||||
/* Button improvements */
|
||||
.action-button {
|
||||
margin-top: 20px;
|
||||
padding: 8px;
|
||||
width: 100px;
|
||||
font-size: 20px;
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.action-button:hover {
|
||||
background-color: #0069d9;
|
||||
}
|
||||
|
||||
.action-button:disabled {
|
||||
background-color: #6c757d;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
489
src/App.jsx
489
src/App.jsx
@@ -1,40 +1,25 @@
|
||||
import './App.css';
|
||||
import { read, utils } from 'xlsx';
|
||||
import { ethers, formatEther, parseEther } from 'ethers';
|
||||
import { read, utils } from "xlsx";
|
||||
import { ethers, parseEther,formatEther } from "ethers";
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { tokenAbi } from './tokenAbi';
|
||||
|
||||
const contractAddress = '0x1ebA64fDe3BF54545c86B9e3bB40c72f50f8D012';
|
||||
import { Table } from './component/table';
|
||||
import { getBalance, multisend } from './utile/blockchain';
|
||||
import { divide } from './utile/chunk';
|
||||
import { setComma } from './utile/setComma';
|
||||
import { List } from './component/list';
|
||||
|
||||
function App() {
|
||||
const abi = [
|
||||
{
|
||||
inputs: [
|
||||
{ internalType: 'address', name: 'token', type: 'address' },
|
||||
{ internalType: 'address[]', name: 'addresses', type: 'address[]' },
|
||||
{ internalType: 'uint256[]', name: 'amounts', type: 'uint256[]' },
|
||||
{ internalType: 'bool', name: 'isToken', type: 'bool' },
|
||||
],
|
||||
name: 'batchTransferFrom',
|
||||
outputs: [{ internalType: 'bool', name: '', type: 'bool' }],
|
||||
stateMutability: 'payable',
|
||||
type: 'function',
|
||||
},
|
||||
];
|
||||
|
||||
const [from, setFrom] = useState('');
|
||||
const [toArray, setToArray] = useState([]);
|
||||
const [amountArray, setAmountArray] = useState([]);
|
||||
const [transferStatuses, setTransferStatuses] = useState([]);
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
const isToken = true;
|
||||
const [from, setFrom] = useState("");
|
||||
const [provider, setProvider] = useState();
|
||||
const [text, setText] = useState('파일 업로드');
|
||||
const tokenRef = useRef(null);
|
||||
const [chunkArray, setChunkArray] = useState([]);
|
||||
const [selectedChunks, setSelectedChunks] = useState([]); // 선택된 청크 인덱스 배열
|
||||
const [balance, setBalance] = useState(0);
|
||||
const [totalAmount, setTotalAmount] = useState(0);
|
||||
|
||||
const handleFile = (file) => {
|
||||
if (!file) return;
|
||||
setText(file.name);
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = (evt) => {
|
||||
@@ -43,363 +28,237 @@ function App() {
|
||||
const workbook = read(data, { type: 'array' });
|
||||
const sheetName = workbook.SheetNames[0];
|
||||
const worksheet = workbook.Sheets[sheetName];
|
||||
const jsonData = utils.sheet_to_json(worksheet, { raw: true });
|
||||
const jsonData = utils.sheet_to_json(worksheet, {raw:true});
|
||||
let total = totalAmount;
|
||||
|
||||
const filteredData = jsonData.filter((row) => {
|
||||
return (
|
||||
typeof row.address === 'string' &&
|
||||
!row.address.toUpperCase().includes('EX') &&
|
||||
!String(row.amount).toUpperCase().includes('EX')
|
||||
);
|
||||
const filteredData = jsonData.filter(row => {
|
||||
if (typeof row.address !== "string" ||
|
||||
row.address.toUpperCase().includes("EX") ||
|
||||
String(row.amount).toUpperCase().includes("EX")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
ethers.getAddress(row.address.split("\r\n").join("").trim());
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.warn(`Invalid address: ${row.address}`, error);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
const addresses = filteredData.map((row) => row.address.trim());
|
||||
const addresses = filteredData.map((row) => {
|
||||
const a = row.address;
|
||||
return a;
|
||||
});
|
||||
const amount = filteredData.map((row) => {
|
||||
// 소수점 2자리까지 반올림 처리
|
||||
const rounded = Math.round(Number(row.amount) * 100) / 100;
|
||||
total += rounded
|
||||
return parseEther(rounded.toFixed(2));
|
||||
});
|
||||
|
||||
setChunkArray(prev => [...prev, ...divide(addresses, amount)]);
|
||||
console.log(total)
|
||||
setTotalAmount(total)
|
||||
|
||||
// Initialize transfer statuses for each address
|
||||
const statuses = addresses.map(() => ({
|
||||
status: 'pending',
|
||||
message: '',
|
||||
}));
|
||||
|
||||
setToArray(addresses);
|
||||
setAmountArray(amount);
|
||||
setTransferStatuses(statuses);
|
||||
setErrorMessage(''); // Clear any previous errors
|
||||
} catch (error) {
|
||||
setErrorMessage(`파일 처리 오류: ${error.message}`);
|
||||
console.error("Error:", error);
|
||||
alert(error)
|
||||
}
|
||||
|
||||
};
|
||||
reader.readAsArrayBuffer(file);
|
||||
};
|
||||
|
||||
const handleFileChange = (e) => {
|
||||
const file = e.target.files[0];
|
||||
handleFile(file);
|
||||
};
|
||||
|
||||
const handleDragOver = (e) => {
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
const handleDrop = (e) => {
|
||||
e.preventDefault();
|
||||
const file = e.dataTransfer.files[0];
|
||||
handleFile(file);
|
||||
};
|
||||
|
||||
const handleFileChange = (e) => {
|
||||
const file = e.target.files[0];
|
||||
handleFile(file);
|
||||
};
|
||||
useEffect(()=>{
|
||||
if (!window.ethereum) return alert("MetaMask not installed");
|
||||
setProvider(new ethers.BrowserProvider(window.ethereum))
|
||||
},[])
|
||||
|
||||
// const handleCheckboxChange = (e) => {
|
||||
// setIsToken(e.target.checked);
|
||||
// };
|
||||
|
||||
const handleDragOver = (e) => {
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!window.ethereum)
|
||||
return setErrorMessage('MetaMask가 설치되지 않았습니다.');
|
||||
setProvider(new ethers.BrowserProvider(window.ethereum));
|
||||
}, []);
|
||||
|
||||
const walletProvider = useCallback(async () => {
|
||||
|
||||
const walletProvider = useCallback(async ()=>{
|
||||
try {
|
||||
await window.ethereum.request({
|
||||
method: 'wallet_switchEthereumChain',
|
||||
params: [{ chainId: '0x38' }], //0x38 56
|
||||
params: [{ chainId: '0x38' }] //0x38
|
||||
});
|
||||
} catch (err) {
|
||||
await window.ethereum.request({
|
||||
method: 'wallet_addEthereumChain',
|
||||
params: [
|
||||
{
|
||||
chainId: '0x38', //0x38
|
||||
chainName: 'BNB Smart Chain Mainnet',
|
||||
rpcUrls: ['https://binance.llamarpc.com'],
|
||||
nativeCurrency: {
|
||||
name: 'BNB',
|
||||
symbol: 'BNB',
|
||||
decimals: 18,
|
||||
},
|
||||
blockExplorerUrls: ['https://bscscan.com/'],
|
||||
params: [{
|
||||
chainId: '0x38', //0x38
|
||||
chainName: 'BNB Smart Chain Mainnet',
|
||||
rpcUrls: ['https://binance.llamarpc.com'],
|
||||
nativeCurrency: {
|
||||
name: 'BNB',
|
||||
symbol: 'BNB',
|
||||
decimals: 18
|
||||
},
|
||||
],
|
||||
blockExplorerUrls: ['https://bscscan.com/']
|
||||
}]
|
||||
});
|
||||
}
|
||||
|
||||
await provider.send("eth_requestAccounts", []);
|
||||
|
||||
await provider.send('eth_requestAccounts', []);
|
||||
|
||||
window.ethereum.on('accountsChanged', (accounts) => {
|
||||
window.ethereum.on("accountsChanged", (accounts) => {
|
||||
if (accounts.length > 0) {
|
||||
setFrom(accounts[0]);
|
||||
} else {
|
||||
setFrom(null);
|
||||
}
|
||||
});
|
||||
const signer = provider.getSigner();
|
||||
const signer = provider.getSigner();
|
||||
const senderAddress = await signer;
|
||||
setFrom(senderAddress.address);
|
||||
}, [provider]);
|
||||
|
||||
useEffect(() => {
|
||||
if (provider) {
|
||||
walletProvider();
|
||||
const tokenAddress = tokenRef.current?.value;
|
||||
const balance = await getBalance(tokenAddress, provider, senderAddress)
|
||||
|
||||
setBalance(balance)
|
||||
setFrom(senderAddress.address)
|
||||
|
||||
},[provider])
|
||||
|
||||
useEffect(()=>{
|
||||
if(provider){
|
||||
walletProvider()
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (window.ethereum?.removeListener) {
|
||||
window.ethereum.removeListener('accountsChanged', () => {});
|
||||
window.ethereum.removeListener('chainChanged', () => {});
|
||||
window.ethereum.removeListener("accountsChanged", () => {});
|
||||
window.ethereum.removeListener("chainChanged", () => {});
|
||||
}
|
||||
};
|
||||
}, [provider, walletProvider]);
|
||||
|
||||
},[provider, walletProvider])
|
||||
|
||||
// 체크박스 토글
|
||||
const toggleChunkSelection = (idx) => {
|
||||
if (chunkArray[idx].status === 0) return;
|
||||
|
||||
setSelectedChunks(prev => {
|
||||
if (prev.includes(idx)) {
|
||||
return prev.filter(i => i !== idx);
|
||||
} else {
|
||||
return [...prev, idx];
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const sendToken = async () => {
|
||||
const tokenAddress = tokenRef.current?.value;
|
||||
let failedIdxs = [];
|
||||
|
||||
let totalAmount = 0;
|
||||
amountArray.map((row) => (totalAmount += parseFloat(formatEther(row))));
|
||||
for (const idx of selectedChunks) {
|
||||
const chunk = chunkArray[idx];
|
||||
|
||||
const signer = await provider.getSigner();
|
||||
const contract = new ethers.Contract(contractAddress, abi, signer);
|
||||
|
||||
const batchSize = 100;
|
||||
const loopCount = Math.ceil(toArray.length / batchSize);
|
||||
|
||||
let status;
|
||||
|
||||
if (
|
||||
toArray.length !== 0 &&
|
||||
amountArray.length !== 0 &&
|
||||
toArray.length === amountArray.length
|
||||
) {
|
||||
try {
|
||||
const tokenContract = new ethers.Contract(
|
||||
const status = await multisend(
|
||||
tokenAddress,
|
||||
tokenAbi,
|
||||
signer
|
||||
provider,
|
||||
from,
|
||||
chunk.chunkAddresses,
|
||||
chunk.chunkAmounts
|
||||
);
|
||||
|
||||
let balance = isToken
|
||||
? await tokenContract.balanceOf(from)
|
||||
: await provider.getBalance(from);
|
||||
console.log(balance, parseEther(totalAmount.toString()));
|
||||
if (balance >= parseEther(totalAmount.toString())) {
|
||||
if (isToken) {
|
||||
//token approve
|
||||
const tx = await tokenContract.approve(
|
||||
contractAddress,
|
||||
parseEther(totalAmount.toString())
|
||||
);
|
||||
await provider.waitForTransaction(tx.hash);
|
||||
}
|
||||
if (status) {
|
||||
setChunkArray(prev => prev.map((c, i) =>
|
||||
i === idx ? {...c, status: 0} : c
|
||||
));
|
||||
} else {
|
||||
throw new Error('잔액부족');
|
||||
setChunkArray(prev => prev.map((c, i) =>
|
||||
i === idx ? {...c, status: 1} : c
|
||||
));
|
||||
failedIdxs.push(idx)
|
||||
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('failed to send');
|
||||
setErrorMessage(`전송 준비 중 오류: ${error.message}`);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
setErrorMessage('파일 양식이 맞지 않습니다. 주소와 금액을 확인해주세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i = 0; i < loopCount; i++) {
|
||||
const start = i * batchSize;
|
||||
const end = start + batchSize;
|
||||
|
||||
const chunkAddresses = toArray.slice(start, end);
|
||||
const chunkAmounts = amountArray.slice(start, end);
|
||||
const chunkTotal = chunkAmounts.reduce(
|
||||
(acc, amt) => acc + parseFloat(formatEther(amt)),
|
||||
0
|
||||
);
|
||||
console.log(
|
||||
tokenAddress,
|
||||
chunkAddresses,
|
||||
chunkAmounts,
|
||||
isToken,
|
||||
chunkTotal
|
||||
);
|
||||
|
||||
//multisender
|
||||
try {
|
||||
const tx = await contract.batchTransferFrom(
|
||||
tokenAddress,
|
||||
chunkAddresses,
|
||||
chunkAmounts,
|
||||
isToken,
|
||||
{ value: isToken ? 0 : parseEther(chunkTotal.toString()) }
|
||||
);
|
||||
|
||||
const result = await provider.waitForTransaction(tx.hash);
|
||||
|
||||
// Update statuses for this batch
|
||||
if (result && result.status === 1) {
|
||||
// Success - update all addresses in this batch to success
|
||||
setTransferStatuses((prev) => {
|
||||
const newStatuses = [...prev];
|
||||
for (let j = start; j < end && j < newStatuses.length; j++) {
|
||||
newStatuses[j] = { status: 'success', message: '전송 성공' };
|
||||
}
|
||||
return newStatuses;
|
||||
});
|
||||
} else {
|
||||
// Failed - update all addresses in this batch to failed
|
||||
setTransferStatuses((prev) => {
|
||||
const newStatuses = [...prev];
|
||||
for (let j = start; j < end && j < newStatuses.length; j++) {
|
||||
newStatuses[j] = { status: 'failed', message: '트랜잭션 실패' };
|
||||
}
|
||||
return newStatuses;
|
||||
});
|
||||
}
|
||||
|
||||
status = result;
|
||||
} catch (error) {
|
||||
// Update all addresses in this batch to failed with error message
|
||||
setTransferStatuses((prev) => {
|
||||
const newStatuses = [...prev];
|
||||
for (let j = start; j < end && j < newStatuses.length; j++) {
|
||||
newStatuses[j] = {
|
||||
status: 'failed',
|
||||
message: `전송 실패: ${error.message || '알 수 없는 오류'}`,
|
||||
};
|
||||
}
|
||||
return newStatuses;
|
||||
});
|
||||
|
||||
setErrorMessage(
|
||||
`배치 전송 실패: ${error.message || '알 수 없는 오류'}`
|
||||
);
|
||||
console.error(`Chunk ${idx + 1} 전송 실패:`, error);
|
||||
alert(`Chunk ${idx + 1} 전송 중 오류가 발생했습니다.`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (status) {
|
||||
setText('파일 업로드');
|
||||
setToArray([]);
|
||||
setAmountArray([]);
|
||||
}
|
||||
// 전송 완료
|
||||
setSelectedChunks(failedIdxs);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="App">
|
||||
<h1>Admin MultiSender</h1>
|
||||
<div
|
||||
style={{
|
||||
width: '600px',
|
||||
height: '100px',
|
||||
margin: '0 Auto',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
>
|
||||
{/* <div>
|
||||
<span style={{fontSize: "18px"}}>토큰 전송 </span>
|
||||
<input type="checkbox" onChange={handleCheckboxChange} />
|
||||
</div> */}
|
||||
<div>
|
||||
<span style={{ fontSize: '18px' }}>토큰 컨트랙트 : </span>
|
||||
<input
|
||||
type="text"
|
||||
ref={tokenRef}
|
||||
placeholder=""
|
||||
style={{ fontSize: '18px', marginTop: '10px', width: '450px' }}
|
||||
defaultValue={'0x36b8dE7c6B06B3f170003452114f0B8E6BcFEE18'}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<span style={{ fontSize: '18px' }}>보내는 계좌 : </span>
|
||||
<input
|
||||
type="text"
|
||||
placeholder={from}
|
||||
style={{
|
||||
fontSize: '18px',
|
||||
marginTop: '10px',
|
||||
width: '450px',
|
||||
border: 'none',
|
||||
}}
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="App">
|
||||
<div className="wrap">
|
||||
<fieldset>
|
||||
<legend>UPLOAD</legend>
|
||||
|
||||
<input
|
||||
type="file"
|
||||
id="fileInput"
|
||||
style={{ display: 'none' }}
|
||||
onChange={handleFileChange}
|
||||
/>
|
||||
<label htmlFor="fileInput">
|
||||
<div
|
||||
id="upload"
|
||||
className="upload-area"
|
||||
onDrop={handleDrop}
|
||||
onDragOver={handleDragOver}
|
||||
>
|
||||
{text}
|
||||
</div>
|
||||
</label>
|
||||
<ul>
|
||||
<li>
|
||||
<label htmlFor="tokenAddress">토큰 컨트랙트</label>
|
||||
<input type="text" id="tokenAddress" defaultValue="0x36b8dE7c6B06B3f170003452114f0B8E6BcFEE18" ref={tokenRef}/>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<label htmlFor="contract">발송 주소</label>
|
||||
<input type="text" id="contract" value={from} disabled/>
|
||||
</li>
|
||||
|
||||
<button type="button" className="action-button" onClick={sendToken}>
|
||||
전송
|
||||
</button>
|
||||
|
||||
{/* Display addresses with transfer status */}
|
||||
{toArray.length > 0 && (
|
||||
<div className="status-container">
|
||||
<h3 className="status-header">전송 상태</h3>
|
||||
<div className="status-list">
|
||||
{toArray.map((address, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`status-item status-${
|
||||
transferStatuses[index]?.status || 'pending'
|
||||
}`}
|
||||
>
|
||||
<span className="status-address">{address}</span>
|
||||
<div className="status-details">
|
||||
<div className="status-amount">
|
||||
{formatEther(amountArray[index])} 토큰
|
||||
</div>
|
||||
<div className="status-text">
|
||||
{transferStatuses[index]?.status === 'success'
|
||||
? '✓ 성공'
|
||||
: transferStatuses[index]?.status === 'failed'
|
||||
? '✗ 실패'
|
||||
: '대기 중'}
|
||||
</div>
|
||||
{transferStatuses[index]?.status === 'failed' &&
|
||||
transferStatuses[index]?.message && (
|
||||
<div
|
||||
className="status-error"
|
||||
style={{
|
||||
fontSize: '10px',
|
||||
marginTop: '2px',
|
||||
color: '#721c24',
|
||||
}}
|
||||
>
|
||||
{transferStatuses[index].message}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<li>
|
||||
<div onDragOver={handleDragOver} onDrop={handleDrop}>
|
||||
<label htmlFor="file">보내는 계좌</label>
|
||||
<input type="file" id="file" accept=".xls,.xlsx,.csv" onChange={handleFileChange} onDrop={handleDrop} onDragOver={handleDragOver}/>
|
||||
<label htmlFor="file"></label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</li>
|
||||
</ul>
|
||||
<p>balance : {setComma(formatEther(balance))}</p>
|
||||
<p>totalAmount : {setComma(totalAmount)}</p>
|
||||
|
||||
{/* Error message display */}
|
||||
{errorMessage && (
|
||||
<div className="error-container">
|
||||
<span className="error-title">오류:</span>
|
||||
{errorMessage}
|
||||
</div>
|
||||
)}
|
||||
<List chunkArray={chunkArray} selectedChunks={selectedChunks} toggleChunkSelection={toggleChunkSelection}></List>
|
||||
|
||||
<button
|
||||
className='transferButton'
|
||||
type='button'
|
||||
onClick={sendToken}
|
||||
disabled={selectedChunks.length === 0}
|
||||
style={{
|
||||
opacity: selectedChunks.length === 0 ? 0.5 : 1,
|
||||
cursor: selectedChunks.length === 0 ? "not-allowed" : "pointer"
|
||||
}}
|
||||
>
|
||||
전송 ({selectedChunks.length})
|
||||
</button>
|
||||
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend>DETAIL</legend>
|
||||
|
||||
<div className="flex-row">
|
||||
{selectedChunks.length > 0 ? (
|
||||
<Table
|
||||
addresses={selectedChunks.flatMap(idx => chunkArray[idx].chunkAddresses)}
|
||||
amounts={selectedChunks.flatMap(idx => chunkArray[idx].chunkAmounts)}
|
||||
/>
|
||||
) : ''}
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
||||
|
||||
59
src/component/list.jsx
Normal file
59
src/component/list.jsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import { setComma } from "../utile/setComma"
|
||||
|
||||
export const List = ({ chunkArray,selectedChunks,toggleChunkSelection }) => {
|
||||
return (
|
||||
<>
|
||||
{chunkArray.length > 0 && (
|
||||
<div style={{marginTop: "20px"}}>
|
||||
<h3>Chunks ({chunkArray.length})</h3>
|
||||
{chunkArray.map((chunk, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
style={{
|
||||
padding: "10px",
|
||||
margin: "5px 0",
|
||||
border: "1px solid #ddd",
|
||||
backgroundColor: chunk.status === 0 ? "#f0f0f0" : "white",
|
||||
borderRadius: "4px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between"
|
||||
}}
|
||||
>
|
||||
<div style={{display: "flex", alignItems: "center"}}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedChunks.includes(idx)}
|
||||
onChange={() => toggleChunkSelection(idx)}
|
||||
disabled={chunk.status === 0}
|
||||
style={{
|
||||
marginRight: "10px",
|
||||
cursor: chunk.status === 0 ? "not-allowed" : "pointer"
|
||||
}}
|
||||
/>
|
||||
<span style={{color: chunk.status === 0 ? "#999" : "black"}}>
|
||||
Chunk {idx + 1}: {chunk.chunkAddresses.length} addresses
|
||||
- Total: {setComma(chunk.chunkTotal.toFixed(2))} tokens
|
||||
</span>
|
||||
</div>
|
||||
{chunk.status === 0 ? (
|
||||
<span style={{color: "green", fontWeight: "bold"}}>
|
||||
✓ 전송완료
|
||||
</span>
|
||||
) :
|
||||
chunk.status === 1 ?
|
||||
(
|
||||
<span style={{color: "red", fontWeight: "bold"}}>
|
||||
X 전송실패
|
||||
</span>
|
||||
) : ''
|
||||
}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
26
src/component/table.jsx
Normal file
26
src/component/table.jsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import { formatEther } from "ethers";
|
||||
|
||||
export const Table = ({ addresses, amounts }) => {
|
||||
return (
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th >Address</th>
|
||||
<th >Amount</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{
|
||||
addresses?.map((address, index) => (
|
||||
<tr key={index}>
|
||||
<td >{address}</td>
|
||||
<td >{formatEther(amounts[index])}</td>
|
||||
</tr>
|
||||
))
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
217
src/index.css
217
src/index.css
@@ -11,3 +11,220 @@ code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
monospace;
|
||||
}
|
||||
|
||||
:root {
|
||||
--gap: 20px;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
margin: 0;
|
||||
font-family: sans-serif;
|
||||
font-size: 12px;
|
||||
scroll-snap-type: y mandatory;
|
||||
scroll-behavior: smooth;
|
||||
scroll-padding-top: 0;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
|
||||
html, body {
|
||||
margin: 0;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
.wrap {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: start;
|
||||
padding: var(--gap);
|
||||
gap: var(--gap);
|
||||
}
|
||||
|
||||
.wrap fieldset {
|
||||
flex-grow: 1;
|
||||
padding: 1.5rem;
|
||||
border: 1px solid #CCC;
|
||||
border-radius: 0.5rem;
|
||||
height: calc(100vh - 80px);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.wrap fieldset:first-child {
|
||||
flex-grow: 0;
|
||||
flex-basis: 500px;
|
||||
}
|
||||
|
||||
.wrap fieldset legend {
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
padding: 0 2rem 0 .75rem;
|
||||
}
|
||||
|
||||
|
||||
fieldset ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
fieldset ul > li {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
fieldset label {
|
||||
font-size: 1.2rem;
|
||||
font-weight: bold;
|
||||
margin-bottom: 0.5rem;
|
||||
padding-left: 0.375rem;
|
||||
}
|
||||
|
||||
fieldset div label {
|
||||
font-size: 1.2rem;
|
||||
font-weight: bold;
|
||||
margin-bottom: 0.5rem;
|
||||
padding-left: 0.375rem;
|
||||
}
|
||||
|
||||
fieldset label+input {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 3rem;
|
||||
padding: .375rem .75rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 400;
|
||||
line-height: 1.5;
|
||||
color: #495057;
|
||||
background-color: #fff;
|
||||
background-clip: padding-box;
|
||||
border: 1px solid #ced4da;
|
||||
border-radius: .5rem;
|
||||
transition: border-color .15s ease-in-out, box-shadow .15s ease-in-out;
|
||||
}
|
||||
|
||||
fieldset div label+input {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 3rem;
|
||||
padding: .375rem .75rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 400;
|
||||
line-height: 1.5;
|
||||
color: #495057;
|
||||
background-color: #fff;
|
||||
background-clip: padding-box;
|
||||
border: 1px solid #ced4da;
|
||||
border-radius: .5rem;
|
||||
transition: border-color .15s ease-in-out, box-shadow .15s ease-in-out;
|
||||
}
|
||||
|
||||
fieldset label+input:focus {
|
||||
color: #495057;
|
||||
background-color: #fff;
|
||||
border-color: #808080;
|
||||
outline: 0;
|
||||
box-shadow: 0 0 0 .2rem rgba(100, 100, 100, .25);
|
||||
}
|
||||
|
||||
fieldset label+input[type="file"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
fieldset input[type="file"]+label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 10rem;
|
||||
border: 2px dotted #CCC;
|
||||
border-radius: .5rem;
|
||||
}
|
||||
|
||||
fieldset input[type="file"]+label::before {
|
||||
content: '업로드 할 파일을 선택하거나 드레그&드롭하세요.';
|
||||
color: rgba(0,0,0,.5);
|
||||
font-weight: 400;
|
||||
text-shadow: 0 0 2px rgba(0,0,0,.7);
|
||||
}
|
||||
|
||||
fieldset table {
|
||||
display: table;
|
||||
width: 100%;
|
||||
caption-side: bottom;
|
||||
border-collapse: separate;
|
||||
box-sizing: border-box;
|
||||
border-spacing: 1px;
|
||||
border-color: gray;
|
||||
background-color: #aaa;
|
||||
font-size: 1.2rem;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
fieldset thead {
|
||||
background-color: rgba(0,0,0,.7);
|
||||
border-bottom: 2px solid rgba(0,0,0,1);
|
||||
color: white;
|
||||
}
|
||||
fieldset th {
|
||||
text-align: center;
|
||||
padding: .375rem .725rem;
|
||||
}
|
||||
fieldset td {
|
||||
background-color: rgba(255,255,255,.9);
|
||||
padding: .2rem .5rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
fieldset tr[row-type="sum"] {
|
||||
border-top: 2px solid #aaa;
|
||||
}
|
||||
|
||||
[data-type="number"] {
|
||||
text-align: right;
|
||||
padding: .2rem 1rem;
|
||||
}
|
||||
|
||||
/* row 처리 */
|
||||
.flex-row {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.flex-row > table {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
|
||||
@media screen and (max-width: 1200px) {
|
||||
|
||||
.wrap {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.wrap fieldset {
|
||||
flex-grow: 1 !important;
|
||||
flex-basis: unset;
|
||||
width: 100%;
|
||||
height: unset;
|
||||
}
|
||||
|
||||
.flex-row {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.transferButton {
|
||||
margin-top: 20px;
|
||||
padding: 8px;
|
||||
width: 100px;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,4 +14,4 @@ root.render(
|
||||
// If you want to start measuring performance in your app, pass a function
|
||||
// to log results (for example: reportWebVitals(console.log))
|
||||
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
|
||||
reportWebVitals(console.log);
|
||||
reportWebVitals();
|
||||
|
||||
72
src/utile/blockchain.js
Normal file
72
src/utile/blockchain.js
Normal file
@@ -0,0 +1,72 @@
|
||||
import { tokenAbi } from "../tokenAbi"
|
||||
import { ethers,formatEther,parseEther } from "ethers";
|
||||
|
||||
const contractAddress = "0x1ebA64fDe3BF54545c86B9e3bB40c72f50f8D012";
|
||||
const abi = [{"inputs": [{"internalType": "address","name": "token","type": "address"},{"internalType": "address[]","name": "addresses","type": "address[]"},{"internalType": "uint256[]","name": "amounts","type": "uint256[]"},{"internalType": "bool","name": "isToken","type": "bool"}],"name": "batchTransferFrom","outputs": [{"internalType": "bool","name": "","type": "bool"}],"stateMutability": "payable","type": "function"}]
|
||||
|
||||
|
||||
export const multisend = async (tokenAddress, provider, from, toArray, amountArray) => {
|
||||
let totalAmount = 0;
|
||||
let isToken = true;
|
||||
let status;
|
||||
|
||||
//console.log(tokenAddress, provider, from)
|
||||
|
||||
amountArray.map((row)=> totalAmount += parseFloat(formatEther(row)));
|
||||
|
||||
const signer = await provider.getSigner();
|
||||
const contract = new ethers.Contract(contractAddress, abi, signer);
|
||||
|
||||
if(toArray.length !== 0 && amountArray.length !== 0 && toArray.length === amountArray.length)
|
||||
{
|
||||
try {
|
||||
const tokenContract = new ethers.Contract(tokenAddress, tokenAbi, signer);
|
||||
|
||||
let balance = isToken ? await tokenContract.balanceOf(from) : await provider.getBalance(from);
|
||||
if(balance >= parseEther(totalAmount.toString())){
|
||||
if(isToken)
|
||||
{
|
||||
////token approve
|
||||
// const tx = await tokenContract.approve(
|
||||
// contractAddress,
|
||||
// parseEther(totalAmount.toString())
|
||||
// )
|
||||
// await provider.waitForTransaction(tx.hash);
|
||||
}
|
||||
} else {
|
||||
throw new Error("잔액부족")
|
||||
}
|
||||
} catch (error) {
|
||||
alert(error)
|
||||
}
|
||||
|
||||
try {
|
||||
const tx = await contract.batchTransferFrom(
|
||||
tokenAddress,
|
||||
toArray,
|
||||
amountArray,
|
||||
isToken,
|
||||
{value : isToken ? 0 : parseEther(totalAmount.toString())}
|
||||
);
|
||||
|
||||
status = await provider.waitForTransaction(tx.hash);
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
} else {
|
||||
alert("양식이 맞지 않습니다.")
|
||||
}
|
||||
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
export const getBalance = async (tokenAddress, provider, address) => {
|
||||
const signer = await provider.getSigner();
|
||||
|
||||
const tokenContract = new ethers.Contract(tokenAddress, tokenAbi, signer);
|
||||
|
||||
let balance = await tokenContract.balanceOf(address)
|
||||
|
||||
return balance;
|
||||
}
|
||||
33
src/utile/chunk.js
Normal file
33
src/utile/chunk.js
Normal file
@@ -0,0 +1,33 @@
|
||||
import { formatEther } from "ethers"
|
||||
|
||||
|
||||
export const divide = (toArray, amountArray) => {
|
||||
const bachArray = [];
|
||||
|
||||
const batchSize = 200;
|
||||
const loopCount = Math.ceil(toArray.length / batchSize);
|
||||
|
||||
for (let i = 0; i < loopCount; i++)
|
||||
{
|
||||
const start = i * batchSize;
|
||||
const end = start + batchSize;
|
||||
|
||||
const chunkAddresses = toArray.slice(start, end)
|
||||
.map(addr => addr.trim())
|
||||
const chunkAmounts = amountArray.slice(start, end);
|
||||
const chunkTotal = chunkAmounts.reduce((acc, amt) => acc + parseFloat(formatEther(amt)), 0);
|
||||
|
||||
let chunkData =
|
||||
{
|
||||
chunkAddresses,
|
||||
chunkAmounts,
|
||||
chunkTotal,
|
||||
status: 2 // 전송 완료 여부
|
||||
}
|
||||
|
||||
bachArray.push(chunkData)
|
||||
}
|
||||
|
||||
return bachArray
|
||||
|
||||
}
|
||||
24
src/utile/setComma.js
Normal file
24
src/utile/setComma.js
Normal file
@@ -0,0 +1,24 @@
|
||||
export const setComma = (value) => {
|
||||
if (value == null) return "0";
|
||||
|
||||
const num = typeof value === "string" ? parseFloat(value) : value;
|
||||
|
||||
if (isNaN(num)) return value;
|
||||
|
||||
return num.toLocaleString("en-US", {
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 2
|
||||
});
|
||||
};
|
||||
|
||||
export const setCommaRegex = (value) => {
|
||||
if (value == null) return "0";
|
||||
|
||||
const str = String(value);
|
||||
const parts = str.split(".");
|
||||
|
||||
// 정수 부분에 콤마 추가
|
||||
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
||||
|
||||
return parts.join(".");
|
||||
};
|
||||
Reference in New Issue
Block a user