진행 과정:
1. 틀 만들기
2. 손절가 계산
3. 알림 기능 --- 현재
알림 기능 개발 과정:
1. 알림 기능 정의
- 손절가에 왔을 때 이를 알아차리고
- 알림 요청 받은 사용자에게 알림 전송
2. 알림 요청 받는 방법
- 로그인된 사용자
-- 알림 요청 받을 디스코드 ID를 적었으면, 그 디스코드 ID를 이용해서 알림 보냄
--- 아이디 수정 가능하도록 함
-- 알림 요청 받을 디스코드 ID가 없으면, 디스코드 ID 입력창 띄움
- 로그인 안된 사용자
-- 로그인 하도록 요청
3. 손절가 왔을 때 알아차릴 방법
- 거래소 선택
- 확인 주기 선택
-- 무료: 분단위
-- 유료: 초단위
- 확인 방법: 매 분 또는 매 초 REST API 요청 후 가격 확인
- scale-up ready: dedicated runtime 을 만들어서. 얘네들만 새로 띄움. DB 공유.
4. 팝업 UI 기획
- 기획
- 종목: (자신이 선택한 종목이 맞는지 확인용)
- 주기: 선택
- 방법: 디스코드 (1차)
- 방법 선택 시 팝업: 디스코드 아이디 입력창
5. 신청 버튼 동작 기획
- POST 요청:
=> 종목, 주기, 방법, 디스코드 ID
- Endpoint:
=> 봇이 보는 DB 업데이트
- 값 확인:
=> 입력받은 디스코드 아이디 없는지 확인
6. dedicated runtime 기획
- 1차로는 nodejs 에서 discord bot과 통신하는 통로 개설
=> 신청 버튼 Endpoint 동작: 1차로는 discord bot에게 입력 받은 내용 전달
- 1차로는 discord bot에서 discord user로 테스트 메시지 보내기
=> 종목, 주기, 방법으로 알림이 신청되었다는 메시지를 입력받은 디스코드 ID로 보내기
7. 데이터 객체 기획
- DB: 1차로는 in-memory
- entity:
-- user: 1차로는 로그인 되었다고 가정
-- subscription: stop-loss price, discord id, interval, ticker, exchange (1차로는 upbit 라고 가정)
8. 구현 진행
- 팝업 창 틀 만들기:
=> cursor composer 사용 modal 만듬
=> three selector input 만듬
=> input 앞에 label 만듬
=> modal 맨 밑에 submit button 만듬
- 알림 신청 버튼 클릭 시 창 띄우기:
=> cursor composer 사용
=> feedback: 아직 hover 해서 page 오른쪽 메뉴 아래에 뜨지 않음
- 알람 방법에서 디스코드 선택시 창 띄우기:
=> cursor composer 사용
- 디스코드 봇 생성 및 권한 부여:
=> ChatGPT 사용해서 따름: 예시 prompt
- 디스코드 메시지 보내는 로직 구현:
=> discord.js pkg 설치
==> terminal:
npm install discord.js
npm install --save-dev @types/node
=> bot token 설정
==> env.local 수정:
DISCORD_BOT_TOKEN=your-bot-token-here
=> client 생성
==> /app/api/discord/send/route.ts 생성
import { NextResponse } from 'next/server';
import { Client, GatewayIntentBits, GuildMember } from 'discord.js';
// Initialize the Discord client
const client = new Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMembers,
GatewayIntentBits.DirectMessages,
],
});
// Log in the Discord client
const BOT_TOKEN = process.env.DISCORD_BOT_TOKEN || '';
if (!BOT_TOKEN) {
throw new Error('Discord bot token is missing from environment variables.');
}
client.login(BOT_TOKEN).catch((error) => {
console.error('Failed to log in Discord client:', error);
});
=> server nickname 으로 discord id 얻기
==> /app/api/discord/send/route.ts 수정
async function getUserIdByNickname(nickname: string): Promise<string | null> {
try {
const guilds = client.guilds.cache;
for (const guild of guilds.values()) {
const members = await guild.members.fetch(); // Fetch all members in the guild
const member = members.find((m: GuildMember) => m.nickname === nickname);
if (member) {
return member.user.id;
}
}
console.warn(`No user found with nickname "${nickname}" across all guilds.`);
return null;
} catch (error) {
console.error(`Error searching for user by nickname "${nickname}":`, error);
return null;
}
}
=> discord id로 메시지 전송
==> /app/api/discord/send/route.ts 수정
=> api route handler
==> /app/api/discord/send/route.ts 수정
// API route handler
export async function POST(request: Request) {
try {
const { nickname, message } = await request.json();
// Input validation
if (!nickname || typeof nickname !== 'string') {
return NextResponse.json(
{ error: 'Invalid or missing "nickname".' },
{ status: 400 }
);
}
if (!message || typeof message !== 'string') {
return NextResponse.json(
{ error: 'Invalid or missing "message".' },
{ status: 400 }
);
}
// Step 1: Get the user ID by nickname across all guilds
const userId = await getUserIdByNickname(nickname);
if (!userId) {
return NextResponse.json(
{ error: `User with nickname "${nickname}" not found in any guild.` },
{ status: 404 }
);
}
// Step 2: Send a DM to the user
await sendDM(userId, message);
return NextResponse.json(
{ success: true, message: `DM successfully sent to user with nickname "${nickname}".` },
{ status: 200 }
);
} catch (error) {
console.error('Error in API route handler:', error);
return NextResponse.json(
{ error: 'Failed to send DM. Please check the server logs for details.' },
{ status: 500 }
);
}
- 테스트 진행:
=> app/discord/send/page.tsx 생성
'use client';
import { useState } from 'react';
export default function SendDMPage() {
const [nickname, setNickname] = useState('');
const [message, setMessage] = useState('');
const [status, setStatus] = useState('');
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setStatus('Sending...');
try {
const response = await fetch('/api/discord/send', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ nickname, message }),
});
const result = await response.json();
if (response.ok) {
setStatus('Message sent successfully!');
} else {
setStatus(`Error: ${result.error}`);
}
} catch (error) {
setStatus('An unexpected error occurred.');
console.error(error);
}
};
return (
<div>
<h1>Send DM to Discord User</h1>
<form onSubmit={handleSubmit}>
<input
type="text"
placeholder="Nickname"
value={nickname}
onChange={(e) => setNickname(e.target.value)}
required
/>
<textarea
placeholder="Message"
value={message}
onChange={(e) => setMessage(e.target.value)}
required
></textarea>
<button type="submit">Send DM</button>
</form>
<p>{status}</p>
</div>
);
}
- 디버깅 진행:
=> zlib-sync compile error
==> discord.js 대신 rest api 요청으로 변경
=> nick 값이 없음. nickname 대신 username 사용
import { NextResponse } from 'next/server';
// Environment variable for the bot token
const BOT_TOKEN = process.env.DISCORD_BOT_TOKEN || '';
if (!BOT_TOKEN) {
throw new Error('Discord bot token is missing from environment variables.');
}
const BASE_URL = 'https://discord.com/api/v10';
// Helper function to make authenticated API requests
async function discordApiRequest(endpoint: string, options: RequestInit = {}) {
const response = await fetch(`${BASE_URL}${endpoint}`, {
...options,
headers: {
Authorization: `Bot ${BOT_TOKEN}`,
'Content-Type': 'application/json',
...options.headers,
},
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(
`Discord API Error: ${response.status} ${response.statusText} - ${JSON.stringify(errorData)}`
);
}
const data = await response.json();
console.log(data);
return data;
}
// Function to get user ID by username
async function getUserIdByUsername(username: string): Promise<string | null> {
try {
const guilds = await discordApiRequest('/users/@me/guilds');
for (const guild of guilds) {
const members = await discordApiRequest(`/guilds/${guild.id}/members?limit=1000`);
const member = members.find((m: any) => (m.user.username) === username);
if (member) {
return member.user.id;
}
}
return null;
} catch (error) {
console.error(`Error finding user by username "${username}":`, error);
return null;
}
}
// Function to send a DM to a user by their ID
async function sendDM(userId: string, message: string): Promise<void> {
try {
const dmChannel = await discordApiRequest('/users/@me/channels', {
method: 'POST',
body: JSON.stringify({ recipient_id: userId }),
});
await discordApiRequest(`/channels/${dmChannel.id}/messages`, {
method: 'POST',
body: JSON.stringify({ content: message }),
});
console.log(`Message successfully sent to user ID ${userId}`);
} catch (error) {
console.error(`Error sending DM to user "${userId}":`, error);
throw error;
}
}
// API route handler
export async function POST(req: Request) {
try {
const { username, message } = await req.json();
// Input validation
if (!username || typeof username !== 'string') {
return NextResponse.json({ error: 'Invalid or missing "username".' }, { status: 400 });
}
if (!message || typeof message !== 'string') {
return NextResponse.json({ error: 'Invalid or missing "message".' }, { status: 400 });
}
// Step 1: Get the user ID by username across all guilds
const userId = await getUserIdByUsername(username);
if (!userId) {
return NextResponse.json(
{ error: `User with username "${username}" not found in any guild.` },
{ status: 404 }
);
}
// Step 2: Send a DM to the user
await sendDM(userId, message);
return NextResponse.json(
{ success: true, message: `DM successfully sent to user with username "${username}".` },
{ status: 200 }
);
} catch (error) {
console.error('Error in API route handler:', error);
return NextResponse.json(
{ error: 'Failed to send DM. Please check the server logs for details.' },
{ status: 500 }
);
}
}
- 팝업 창 내 신청 버튼 로직 구현:
=> api/discord/send 로 username, ticker, interval, stop-loss price 전송
const handleSubmit = async () => {
if (!data) return;
const interval = (document.getElementById('interval-select') as HTMLSelectElement).value;
const method = (document.getElementById('method-select') as HTMLSelectElement).value;
const message = `ticker: ${ticker}, stop-loss price: ${data.swingLowMinusATR.toFixed(2)}, interval: ${interval}, method: ${method}`;
setStatus('Sending...');
try {
const response = await fetch('/api/discord/send', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: discordId, message }),
});
const result = await response.json();
if (response.ok) {
setStatus('Message sent successfully!');
} else {
setStatus(`Error: ${result.error}`);
}
} catch (error) {
setStatus('An unexpected error occurred.');
console.error(error);
}
};
- 테스트 진행 및 결과:
관련 코드 (github):
- https://github.com/tyson-park/crypto-stop-loss-alarm/commit/eb6d5541345c9343ceeb28694b1793d7910a5f2f
이전 글:
'서비스 창작 기록' 카테고리의 다른 글
쉽게 쉽게 코인 투자하기! '이것' 만 지키면 내 돈 지킵니다 (1) | 2024.12.23 |
---|---|
코인 투자, 본능에 갇힌 인간들: 두려움이 만든 '이것' 실패의 비극 (0) | 2024.12.23 |
코인 손절 알람기 만들기 - 2. 손절가 계산 - nextjs, ChatGPT, 업비트 REST API (0) | 2024.12.18 |
코인 손절 알람기 만들기 - 1. 틀 만들기 - react, nextjs, v0, cursor (0) | 2024.12.18 |
ATR를 활용한 효과적인 손절매 설정 방법 (1) | 2024.12.17 |