feat: add common utilities and feedback handling scripts

- Introduced common.js for shared functions including API calls, error reporting, and UI helpers.
- Implemented feedback.js for the feedback reporting page, including form handling and displaying feedback status.
- Added layout.js for managing galaxy and section layouts in the visualization.
- Created login.js for user authentication with session management.
- Developed search.js for term searching based on user input and synonyms.
- Implemented signup.js for user registration with validation and immediate login.
This commit is contained in:
jsahn
2026-10-06 23:14:45 +09:00
parent 04158ac442
commit 4e943700fc
62 changed files with 158771 additions and 0 deletions
+100
View File
@@ -0,0 +1,100 @@
"""피드백·오류를 터미널에서 확인하고 처리하는 도구.
.venv/bin/python -m server.cli feedback # 처리 대기(new, in_progress) 목록
.venv/bin/python -m server.cli feedback --all # 전체
.venv/bin/python -m server.cli show 12 # 한 건 자세히 (대상 용어 현재 값 포함)
.venv/bin/python -m server.cli resolve 12 "정의 문장을 교재 p.188 기준으로 수정"
.venv/bin/python -m server.cli status 12 in_progress
.venv/bin/python -m server.cli term 지역사회 간호사정 # 용어 현재 값 확인
"""
import argparse
import json
from . import db
def _print_fb(r, full=False):
tgt = f" [{r['target_type']}:{r['target_label'] or r['target_id']}]" if r["target_type"] else ""
occ = f" ×{r['occurrences']}" if (r["occurrences"] or 1) > 1 else ""
print(f"#{r['id']:<4} {r['status']:<11} {r['kind']:<12} {r['category'] or ''}{tgt}{occ} {r['created_at']}")
msg = r["message"] or ""
print(" " + (msg if full else msg[:160].replace("\n", " ")))
if r["suggestion"]:
print(" → 제안: " + (r["suggestion"] if full else r["suggestion"][:160]))
if full:
for k in ("page_url", "reporter", "contact", "admin_note", "resolution", "handled_by", "user_agent"):
if r[k]:
print(f" {k}: {r[k]}")
if r["context"]:
print(" context:", r["context"][:3000])
def main():
ap = argparse.ArgumentParser()
sub = ap.add_subparsers(dest="cmd", required=True)
f = sub.add_parser("feedback")
f.add_argument("--all", action="store_true")
f.add_argument("--kind")
s = sub.add_parser("show")
s.add_argument("id", type=int)
r = sub.add_parser("resolve")
r.add_argument("id", type=int)
r.add_argument("resolution")
st = sub.add_parser("status")
st.add_argument("id", type=int)
st.add_argument("status", choices=["new", "in_progress", "resolved", "rejected", "duplicate"])
t = sub.add_parser("term")
t.add_argument("name", nargs="+")
a = ap.parse_args()
con = db.connect()
if a.cmd == "feedback":
where = "1=1" if a.all else "status IN ('new','in_progress')"
args = []
if a.kind:
where += " AND kind=?"
args.append(a.kind)
rows = con.execute(f"SELECT * FROM feedback WHERE {where} ORDER BY id", args).fetchall()
print(f"{len(rows)}건")
for r in rows:
_print_fb(r)
elif a.cmd == "show":
r = con.execute("SELECT * FROM feedback WHERE id=?", (a.id,)).fetchone()
if not r:
return print("없음")
_print_fb(r, full=True)
if r["target_type"] == "term" and r["target_id"]:
_show_term(con, r["target_id"])
elif a.cmd in ("resolve", "status"):
status = "resolved" if a.cmd == "resolve" else a.status
with con:
if a.cmd == "resolve":
con.execute("UPDATE feedback SET status=?, resolution=?, handled_by='developer', updated_at=? WHERE id=?",
(status, a.resolution, db.now(), a.id))
else:
con.execute("UPDATE feedback SET status=?, handled_by='developer', updated_at=? WHERE id=?", (status, db.now(), a.id))
db.audit(con, "developer(cli)", "update", "feedback", str(a.id), None, {"status": status})
print("완료")
elif a.cmd == "term":
name = " ".join(a.name)
r = con.execute("SELECT term_id FROM terms WHERE name=? OR term_id=?", (name, name)).fetchone()
if not r:
rows = con.execute("SELECT term_id,name FROM terms WHERE name LIKE ? LIMIT 20", (f"%{name}%",)).fetchall()
for x in rows:
print(x["term_id"], x["name"])
return
_show_term(con, r["term_id"])
def _show_term(con, term_id):
t = db.row(con.execute("SELECT * FROM terms WHERE term_id=?", (term_id,)).fetchone())
print("\n--- 대상 용어 현재 값")
print(json.dumps({k: t[k] for k in ("term_id", "name", "aliases", "galaxy_id", "section_id", "definition_text",
"definition_book", "definition_page", "easy", "example", "review_status")},
ensure_ascii=False, indent=1))
for s in con.execute("SELECT book_id,page,path,snippet FROM term_sources WHERE term_id=?", (term_id,)):
print(f" 출처 {s['book_id']} p.{s['page']} {s['path']}\n {(s['snippet'] or '')[:150]}")
if __name__ == "__main__":
main()