From a0055c584c57cc5555d38e9f62c5600c0d26ea6a Mon Sep 17 00:00:00 2001 From: walnutpy Date: Thu, 19 Feb 2026 02:23:39 +0900 Subject: [PATCH 1/2] app --- app.py | 118 +++++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 77 insertions(+), 41 deletions(-) diff --git a/app.py b/app.py index 8c3e6c8..c28053b 100644 --- a/app.py +++ b/app.py @@ -17,23 +17,29 @@ app = Flask(__name__) DB_PATH = "candles.db" +PUSH_TOKEN = os.environ.get("PUSH_TOKEN") +if not PUSH_TOKEN: + raise RuntimeError("PUSH_TOKEN not set") + +DB_PATH = os.environ.get("CANDLES_DB_PATH", "candles.db") + PUSH_TOKEN = os.environ.get("PUSH_TOKEN") if not PUSH_TOKEN: raise RuntimeError("PUSH_TOKEN not set") def init_db(): conn = sqlite3.connect(DB_PATH) - c = conn.cursor() - c.execute(""" + cur = conn.cursor() + cur.execute(""" CREATE TABLE IF NOT EXISTS candles ( - code TEXT, - timeframe TEXT, - t TEXT, - o REAL, - h REAL, - l REAL, - c REAL, - v REAL, + code TEXT NOT NULL, + timeframe TEXT NOT NULL, + t TEXT NOT NULL, + o REAL NOT NULL, + h REAL NOT NULL, + l REAL NOT NULL, + c REAL NOT NULL, + v REAL NOT NULL, PRIMARY KEY (code, timeframe, t) ) """) @@ -42,6 +48,7 @@ def init_db(): init_db() + # --------------------------------------------------------------------- # Common # --------------------------------------------------------------------- @@ -498,6 +505,41 @@ def api_stocks_search(): return jsonify({"items": items}) +@app.post("/api/internal/push/candles") +def push_candles(): + token = request.headers.get("X-PUSH-TOKEN", "") + if token != PUSH_TOKEN: + return jsonify({"error": "Unauthorized"}), 403 + + data = request.get_json(silent=True) or {} + code = (data.get("code") or "").strip() + candles = data.get("candles") or [] + + if not re.fullmatch(r"\d{6}", code): + return jsonify({"error": "code must be 6 digits"}), 400 + + if not isinstance(candles, list) or not candles: + return jsonify({"error": "candles must be a non-empty list"}), 400 + + conn = sqlite3.connect(DB_PATH) + cur = conn.cursor() + + for cndl in candles: + try: + t = cndl["t"] + o = float(cndl["o"]); h = float(cndl["h"]); l = float(cndl["l"]); c = float(cndl["c"]); v = float(cndl["v"]) + except Exception: + continue + + cur.execute(""" + INSERT OR REPLACE INTO candles (code, timeframe, t, o, h, l, c, v) + VALUES (?, '1m', ?, ?, ?, ?, ?, ?) + """, (code, t, o, h, l, c, v)) + + conn.commit() + conn.close() + return jsonify({"status": "ok"}) + @app.get("/api/stocks/candles") def api_stocks_candles(): @@ -508,53 +550,47 @@ def api_stocks_candles(): if not re.fullmatch(r"\d{6}", code): return jsonify({"error": "code must be 6 digits"}), 400 - # 프론트 tf -> 네이버 tf 매핑 - # 네이버 siseJson은 day/week/month는 잘 맞는데, 분봉/틱봉은 여기서 못 뽑음. - if tf == "1d": - n_tf = "day" - elif tf == "1w": - n_tf = "week" - elif tf == "1M": - n_tf = "month" - elif tf == "1m": + # ✅ 1m은 DB에서 + if tf == "1m": conn = sqlite3.connect(DB_PATH) - c = conn.cursor() - c.execute(""" + cur = conn.cursor() + cur.execute(""" SELECT t, o, h, l, c, v FROM candles - WHERE code=? + WHERE code=? AND timeframe='1m' ORDER BY t ASC """, (code,)) - rows = c.fetchall() + rows = cur.fetchall() conn.close() - candles = [ - { - "time": row[0], - "open": row[1], - "high": row[2], - "low": row[3], - "close": row[4], - "volume": row[5], - } - for row in rows - ] - return jsonify(candles) + candles = [{ + "time": r[0], + "open": r[1], + "high": r[2], + "low": r[3], + "close": r[4], + "volume": r[5], + } for r in rows][-count:] + return jsonify({"code": code, "name": code, "tf": tf, "candles": candles}) + + # ✅ 1d/1w/1M은 네이버 그대로 + if tf == "1d": + n_tf = "day" + elif tf == "1w": + n_tf = "week" + elif tf == "1M": + n_tf = "month" else: return jsonify({"error": f"unknown tf: {tf}"}), 400 try: candles = fetch_naver_stock_candles(code, tf=n_tf, count=min(max(count, 30), 1200)) - return jsonify({ - "code": code, - "name": code, # TODO: 종목명 매핑 테이블 붙이면 여기서 채우면 됨 - "tf": tf, - "candles": candles, - }) + return jsonify({"code": code, "name": code, "tf": tf, "candles": candles}) except Exception as e: return jsonify({"error": str(e)}), 500 + # --------------------------------------------------------------------- # Routes # --------------------------------------------------------------------- From 10f88322eae26551da6201b1b68e63913cb98ab2 Mon Sep 17 00:00:00 2001 From: walnutpy Date: Thu, 19 Feb 2026 02:30:46 +0900 Subject: [PATCH 2/2] app.py er --- app.py | 115 +++++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 74 insertions(+), 41 deletions(-) diff --git a/app.py b/app.py index c28053b..7a374a3 100644 --- a/app.py +++ b/app.py @@ -18,14 +18,7 @@ DB_PATH = "candles.db" PUSH_TOKEN = os.environ.get("PUSH_TOKEN") -if not PUSH_TOKEN: - raise RuntimeError("PUSH_TOKEN not set") -DB_PATH = os.environ.get("CANDLES_DB_PATH", "candles.db") - -PUSH_TOKEN = os.environ.get("PUSH_TOKEN") -if not PUSH_TOKEN: - raise RuntimeError("PUSH_TOKEN not set") def init_db(): conn = sqlite3.connect(DB_PATH) @@ -43,12 +36,29 @@ def init_db(): PRIMARY KEY (code, timeframe, t) ) """) + + cur.execute(""" + CREATE TABLE IF NOT EXISTS subscriptions ( + code TEXT PRIMARY KEY, + enabled INTEGER NOT NULL DEFAULT 1, + updated_at TEXT NOT NULL + ) + """) + conn.commit() conn.close() init_db() + +def _require_push_token(): + token = request.headers.get("X-PUSH-TOKEN", "") + if token != PUSH_TOKEN: + return jsonify({"error": "Unauthorized"}), 403 + return None + + # --------------------------------------------------------------------- # Common # --------------------------------------------------------------------- @@ -505,6 +515,63 @@ def api_stocks_search(): return jsonify({"items": items}) +@app.post("/api/subscribe") +def api_subscribe(): + payload = request.get_json(silent=True) or {} + code = (payload.get("code") or "").strip() + + if not re.fullmatch(r"\d{6}", code): + return jsonify({"error": "code must be 6 digits"}), 400 + + conn = sqlite3.connect(DB_PATH) + cur = conn.cursor() + cur.execute(""" + INSERT OR REPLACE INTO subscriptions (code, enabled, updated_at) + VALUES (?, 1, ?) + """, (code, datetime.now().isoformat(timespec="seconds"))) + conn.commit() + conn.close() + + return jsonify({"ok": True, "code": code}) + + +@app.post("/api/unsubscribe") +def api_unsubscribe(): + payload = request.get_json(silent=True) or {} + code = (payload.get("code") or "").strip() + + if not re.fullmatch(r"\d{6}", code): + return jsonify({"error": "code must be 6 digits"}), 400 + + conn = sqlite3.connect(DB_PATH) + cur = conn.cursor() + cur.execute("DELETE FROM subscriptions WHERE code=?", (code,)) + conn.commit() + conn.close() + + return jsonify({"ok": True, "code": code}) + +@app.get("/api/internal/subscriptions") +def api_internal_subscriptions(): + auth = _require_push_token() + if auth: + return auth + + conn = sqlite3.connect(DB_PATH) + cur = conn.cursor() + cur.execute(""" + SELECT code + FROM subscriptions + WHERE enabled=1 + ORDER BY updated_at DESC + LIMIT 200 + """) + rows = cur.fetchall() + conn.close() + + return jsonify({"codes": [r[0] for r in rows]}) + + @app.post("/api/internal/push/candles") def push_candles(): token = request.headers.get("X-PUSH-TOKEN", "") @@ -711,40 +778,6 @@ def api_calendar_delete(date: str, event_id: str): _save_calendar(data) return jsonify({"ok": True}) -@app.route("/api/internal/push/candles", methods=["POST"]) -def push_candles(): - token = request.headers.get("X-PUSH-TOKEN") - if token != PUSH_TOKEN: - return {"error": "Unauthorized"}, 403 - - data = request.json - code = data.get("code") - candles = data.get("candles", []) - - conn = sqlite3.connect(DB_PATH) - c = conn.cursor() - - for candle in candles: - c.execute(""" - INSERT OR REPLACE INTO candles - (code, timeframe, t, o, h, l, c, v) - VALUES (?, ?, ?, ?, ?, ?, ?, ?) - """, ( - code, - "1m", - candle["t"], - candle["o"], - candle["h"], - candle["l"], - candle["c"], - candle["v"], - )) - - conn.commit() - conn.close() - - return {"status": "ok"} - # --------------------------------------------------------------------- if __name__ == "__main__":