From 398583cdc1af1f649b374a2887b920dafb422314 Mon Sep 17 00:00:00 2001 From: jiminseon <20201020@dongduk.ac.kr> Date: Tue, 15 Jul 2025 09:16:51 +0900 Subject: [PATCH 1/2] =?UTF-8?q?[chore]=20.gitignore=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 4092687..68e4cd0 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,6 @@ coverage/ # misc .DS_Store + + +tokenCache.json \ No newline at end of file From 70e003fec44289f7e0e0966b431efdff2220796a Mon Sep 17 00:00:00 2001 From: jiminseon <20201020@dongduk.ac.kr> Date: Tue, 15 Jul 2025 09:18:27 +0900 Subject: [PATCH 2/2] =?UTF-8?q?[feat]=20=EB=B3=B4=EC=9C=A0=EC=A3=BC?= =?UTF-8?q?=EC=8B=9D=EC=9D=B4=20=EC=9E=88=EC=9C=BC=EB=A9=B4=20=3F=20?= =?UTF-8?q?=EC=B0=A8=ED=8A=B8=ED=8E=98=EC=9D=B4=EC=A7=80=20:=20=EC=97=B0?= =?UTF-8?q?=EB=8F=99=ED=8E=98=EC=9D=B4=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.js | 6 +-- models/UserStock.js | 13 +++++ package.json | 1 + routes/auth.js | 20 ++++++++ routes/real.js | 106 +++++++++++++++++++++++---------------- routes/users.js | 9 ---- services/tokenService.js | 91 ++++++++++++++++++++++++++------- 7 files changed, 172 insertions(+), 74 deletions(-) create mode 100644 models/UserStock.js delete mode 100644 routes/users.js diff --git a/app.js b/app.js index 2ae05e1..ea208c1 100644 --- a/app.js +++ b/app.js @@ -7,8 +7,8 @@ var logger = require("morgan"); var cors = require("cors"); /* --------------------------------------- */ var indexRouter = require("./routes/index"); -var usersRouter = require("./routes/users"); var authRouter = require("./routes/auth"); +var balanceRouter = require("./routes/real"); /* --------------------------------------- */ const mongoose = require("mongoose"); const dotenv = require("dotenv"); @@ -48,8 +48,6 @@ app.use(express.static(path.join(__dirname, "public"))); /* --------------------------------------- */ app.use("/", indexRouter); app.use("/auth", authRouter); -app.use("/users", usersRouter); -var balanceRouter = require("./routes/real"); app.use("/api/real", balanceRouter); /* --------------------------------------- */ const port = process.env.PORT || 3001; @@ -71,7 +69,7 @@ app.use(function (err, req, res, next) { // render the error page res.status(err.status || 500); - res.render("error"); + // res.render("error"); }); module.exports = app; diff --git a/models/UserStock.js b/models/UserStock.js new file mode 100644 index 0000000..ced0463 --- /dev/null +++ b/models/UserStock.js @@ -0,0 +1,13 @@ +const mongoose = require("mongoose"); + +const userStockSchema = new mongoose.Schema({ + user_id: { + type: mongoose.Schema.Types.ObjectId, + ref: "Auth", + required: true, + }, + stock_code: { type: String }, + cumulative_score: { type: Number }, +}); + +module.exports = mongoose.model("UserStock", userStockSchema); diff --git a/package.json b/package.json index 5382dd0..f9315e5 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "mongodb": "^6.17.0", "mongoose": "^8.16.3", "morgan": "~1.9.1", + "node-fetch": "^2.7.0", "validator": "^13.15.15" } } diff --git a/routes/auth.js b/routes/auth.js index b7975c6..c902ad7 100644 --- a/routes/auth.js +++ b/routes/auth.js @@ -1,6 +1,8 @@ var express = require("express"); const Auth = require("../models/Auth"); const { createToken } = require("../utils/auth"); +const { authenticate } = require("../middleware/auth"); + var router = express.Router(); router.post("/signup", async (req, res) => { @@ -50,4 +52,22 @@ router.all("/logout", (req, res) => { res.status(200).json({ message: "로그아웃되었습니다." }); }); +/* --- 내 정보 확인 --- */ +router.get("/me", authenticate, async (req, res) => { + try { + const user = req.user; // 미들웨어에서 전달된 사용자 정보 + + res.status(200).json({ + success: true, + user: { + email: user.email, + nickname: user.nickname, + }, + }); + } catch (err) { + console.error("내 정보 조회 중 에러 발생:", err); + res.status(500).json({ success: false, message: "서버 에러" }); + } +}); + module.exports = router; diff --git a/routes/real.js b/routes/real.js index 04cae30..ecbdd0a 100644 --- a/routes/real.js +++ b/routes/real.js @@ -1,57 +1,75 @@ const express = require("express"); const router = express.Router(); const { getBalance } = require("../services/stockService"); +const { authenticate } = require("../middleware/auth"); +const UserStock = require("../models/UserStock"); -router.get("/", async (req, res) => { +router.post("/", authenticate, async (req, res) => { const cano = "50143725"; const acnt = "01"; + const userId = req.user._id; try { - const data = await getBalance(cano, acnt); - res.json(data); + const result = await getBalance(cano, acnt); + + const stocks = (result.output1 || []).map((item) => ({ + stock_code: item.pdno, + cumulative_score: 0, + })); + + if (stocks.length === 0) { + return res.status(200).json({ success: true, message: "보유 종목 없음" }); + } + console.log("보유 주식 조회 userId", userId); + + // 중복 없이 저장 + const bulkOps = stocks.map((stock) => ({ + updateOne: { + filter: { user_id: userId, stock_code: stock.stock_code }, + update: { $setOnInsert: { ...stock, user_id: userId } }, + upsert: true, + }, + })); + + await UserStock.bulkWrite(bulkOps); + + res.status(200).json({ + success: true, + message: "계좌 연동 완료", + inserted: stocks.length, + output1: result.output1, + }); } catch (err) { - console.error("잔고 조회 오류:", err.message); - res.status(500).json({ error: "잔고 조회 실패" }); + console.error("계좌 연동 오류:", err.message); + res.status(500).json({ error: "계좌 연동 실패" }); } }); -module.exports = router; +router.get("/", async (req, res) => { + try { + const userStock = await UserStock.find(); + + console.log("보유 주식 DB 조회 완료:", userStock); + res.json({ message: "보유 주식 DB 조회 완료", userStock }); + } catch (err) { + console.error(err); + res.status(500); + next(err); + } +}); + +router.get("/status", authenticate, async (req, res) => { + const userId = req.user._id; + try { + const hasStock = await UserStock.exists({ user_id: userId }); -// const express = require("express"); -// const router = express.Router(); -// const { getBalance } = require("../services/getBalance"); -// const Stock = require("../models/Stock"); -// const UserStock = require("../models/UserStock"); - -// router.post("/", async (req, res) => { -// try { -// const { userId, cano, acntPrdtCd } = req.body; -// const data = await getBalance(cano, acntPrdtCd); -// const stocks = data.output1; - -// for (const item of stocks) { -// const { pdno, prdt_name } = item; - -// // 주식 저장 (중복 처리) -// await Stock.updateOne( -// { stock_code: pdno }, -// { $set: { company: prdt_name, state: true } }, -// { upsert: true } -// ); - -// // 보유 주식 저장 -// await UserStock.updateOne( -// { user_id: userId, stock_code: pdno }, -// { $setOnInsert: { cumulative_score: 0 } }, -// { upsert: true } -// ); -// } - -// res.json({ count: stocks.length, stocks: stocks.map((s) => s.prdt_name) }); -// } catch (err) { -// console.error("잔고 조회 오류:", err.message); -// res.status(500).json({ message: err.message }); -// } -// }); - -// module.exports = router; + res.status(200).json({ + hasHoldings: !!hasStock, // boolean 값으로 변경 + }); + } catch (err) { + console.error("보유 주식 확인 에러:", err); + res.status(500).json({ message: "서버 에러" }); + } +}); + +module.exports = router; diff --git a/routes/users.js b/routes/users.js deleted file mode 100644 index 7981267..0000000 --- a/routes/users.js +++ /dev/null @@ -1,9 +0,0 @@ -var express = require("express"); -var router = express.Router(); - -/* GET users listing. */ -router.get("/", function (req, res, next) { - res.send("respond with a resource"); -}); - -module.exports = router; diff --git a/services/tokenService.js b/services/tokenService.js index 76cf6ea..5bbe31f 100644 --- a/services/tokenService.js +++ b/services/tokenService.js @@ -1,25 +1,82 @@ +const fs = require("fs"); +const path = require("path"); const fetch = require("node-fetch"); +const CACHE_FILE = path.resolve(__dirname, "tokenCache.json"); + let cachedToken = null; +let tokenExpiresAt = null; + +// 서버 시작 시 캐시 파일에서 복구 +(function loadTokenCache() { + try { + const data = fs.readFileSync(CACHE_FILE, "utf-8"); + const parsed = JSON.parse(data); + + if (parsed.token && parsed.expiresAt && Date.now() < parsed.expiresAt) { + cachedToken = parsed.token; + tokenExpiresAt = parsed.expiresAt; + console.log("🔁 캐시된 토큰 복구 성공"); + } + } catch { + // 캐시 없음 + } +})(); + +function saveTokenCache(token, expiresAt) { + fs.writeFileSync(CACHE_FILE, JSON.stringify({ token, expiresAt }), "utf-8"); + console.log("💾 토큰 캐시 저장 완료"); +} async function getAccessToken() { - if (cachedToken) return cachedToken; - - const res = await fetch(`${process.env.TOKEN_DOMAIN}`, { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ - grant_type: "client_credentials", - appkey: process.env.API_APPKEY, - appsecret: process.env.API_APPSECRET, - }), - }); - - if (!res.ok) throw new Error("토큰 발급 실패"); - - const data = await res.json(); - cachedToken = data.access_token; - return cachedToken; + const now = Date.now(); + + if (cachedToken && tokenExpiresAt && now < tokenExpiresAt) { + return cachedToken; + } + + try { + const res = await fetch(process.env.TOKEN_DOMAIN, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + grant_type: "client_credentials", + appkey: process.env.API_APPKEY, + appsecret: process.env.API_APPSECRET, + }), + }); + + if (!res.ok) { + const errorBody = await res.json(); + const isRateLimited = + res.status === 403 && errorBody.error_code === "EGW00133"; + + if (isRateLimited && cachedToken) { + console.warn("⚠️ 토큰 요청 제한. 기존 토큰 재사용"); + return cachedToken; + } + + throw new Error( + `토큰 발급 실패: ${res.status} - ${ + errorBody.error_description || "Unknown" + }` + ); + } + + const data = await res.json(); + cachedToken = data.access_token; + tokenExpiresAt = now + (data.expires_in - 300) * 1000; // 만료 5분 전부터 새로 받기 + + saveTokenCache(cachedToken, tokenExpiresAt); + return cachedToken; + } catch (err) { + console.error("🚨 getAccessToken 오류:", err.message); + if (cachedToken) { + console.warn("🔁 기존 토큰 재사용 (예외 발생)"); + return cachedToken; + } + throw err; + } } module.exports = { getAccessToken };