db ready
parent
d1293f9188
commit
ab215395c6
|
|
@ -0,0 +1,134 @@
|
||||||
|
-- user_info
|
||||||
|
CREATE TABLE user_info
|
||||||
|
(
|
||||||
|
`user_id` INT NOT NULL AUTO_INCREMENT,
|
||||||
|
`username` VARCHAR(50) NOT NULL,
|
||||||
|
`password` VARCHAR(50) NOT NULL,
|
||||||
|
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (user_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
-- hospital_baseinfo
|
||||||
|
CREATE TABLE hospital_baseinfo
|
||||||
|
(
|
||||||
|
`hospital_id` CHAR(36) NOT NULL,
|
||||||
|
`owner_user_id` INT NOT NULL,
|
||||||
|
`hospital_name` VARCHAR(50) NOT NULL,
|
||||||
|
`hospital_name_en` VARCHAR(50) NULL,
|
||||||
|
`brn` VARCHAR(50) NOT NULL,
|
||||||
|
`road_address` VARCHAR(100) NULL,
|
||||||
|
`site_address` VARCHAR(100) NULL,
|
||||||
|
`status` VARCHAR(20) NOT NULL DEFAULT 'start',
|
||||||
|
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (hospital_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IX_hospital_baseinfo_1 ON hospital_baseinfo (owner_user_id);
|
||||||
|
|
||||||
|
|
||||||
|
-- remote_source: 병원별 채널 소스 정보 (instagram/facebook/naver_blog/youtube/gangnam_unni 등)
|
||||||
|
CREATE TABLE remote_source
|
||||||
|
(
|
||||||
|
`source_id` INT NOT NULL AUTO_INCREMENT,
|
||||||
|
`hospital_id` CHAR(36) NOT NULL,
|
||||||
|
`source_type` VARCHAR(50) NOT NULL,
|
||||||
|
`url` VARCHAR(500) NOT NULL,
|
||||||
|
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (source_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IX_remote_source_1 ON remote_source (hospital_id);
|
||||||
|
CREATE INDEX IX_remote_source_2 ON remote_source (hospital_id, source_type);
|
||||||
|
|
||||||
|
|
||||||
|
-- analysis_runs
|
||||||
|
CREATE TABLE analysis_runs
|
||||||
|
(
|
||||||
|
`analysis_run_id` CHAR(36) NOT NULL,
|
||||||
|
`hospital_id` CHAR(36) NOT NULL,
|
||||||
|
`owner_user_id` INT NOT NULL DEFAULT 0,
|
||||||
|
`status` VARCHAR(50) NOT NULL DEFAULT 'discovering',
|
||||||
|
`report_data` JSON NULL,
|
||||||
|
`plan_data` JSON NULL,
|
||||||
|
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (analysis_run_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IX_analysis_runs_1 ON analysis_runs (hospital_id);
|
||||||
|
CREATE INDEX IX_analysis_runs_2 ON analysis_runs (owner_user_id);
|
||||||
|
|
||||||
|
|
||||||
|
-- raw_info: 분석 실행별 수집 원시 데이터
|
||||||
|
CREATE TABLE raw_info
|
||||||
|
(
|
||||||
|
`info_id` INT NOT NULL AUTO_INCREMENT,
|
||||||
|
`source_id` INT NOT NULL,
|
||||||
|
`analysis_run_id` CHAR(36) NOT NULL,
|
||||||
|
`data_tag` VARCHAR(50) NOT NULL DEFAULT 'default',
|
||||||
|
`status` VARCHAR(20) NOT NULL DEFAULT 'start',
|
||||||
|
`raw_data` JSON NULL,
|
||||||
|
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (info_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IX_raw_info_1 ON raw_info (analysis_run_id);
|
||||||
|
CREATE INDEX IX_raw_info_2 ON raw_info (source_id);
|
||||||
|
|
||||||
|
|
||||||
|
-- file_data
|
||||||
|
CREATE TABLE file_data
|
||||||
|
(
|
||||||
|
`id` INT NOT NULL AUTO_INCREMENT,
|
||||||
|
`analysis_run_id` CHAR(36) NOT NULL,
|
||||||
|
`hospital_id` CHAR(36) NULL,
|
||||||
|
`file_type` ENUM('image','video','audio','document','file') NOT NULL DEFAULT 'file',
|
||||||
|
`file_name` VARCHAR(255) NOT NULL,
|
||||||
|
`file_url` VARCHAR(2048) NOT NULL,
|
||||||
|
`size_bytes` BIGINT NULL,
|
||||||
|
`is_deleted` BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
INDEX IX_file_data_1 (analysis_run_id, is_deleted)
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
-- hospital_history
|
||||||
|
CREATE TABLE hospital_history
|
||||||
|
(
|
||||||
|
`id` INT NOT NULL AUTO_INCREMENT,
|
||||||
|
`hospital_id` CHAR(36) NOT NULL,
|
||||||
|
`owner_user_id` INT NOT NULL,
|
||||||
|
`hospital_name` VARCHAR(50) NOT NULL,
|
||||||
|
`hospital_name_en` VARCHAR(50) NULL,
|
||||||
|
`brn` VARCHAR(50) NOT NULL,
|
||||||
|
`road_address` VARCHAR(100) NULL,
|
||||||
|
`site_address` VARCHAR(100) NULL,
|
||||||
|
`status` VARCHAR(20) NOT NULL,
|
||||||
|
`analysis_run_id` CHAR(36) NULL,
|
||||||
|
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IX_hospital_history_1 ON hospital_history (hospital_id);
|
||||||
|
CREATE INDEX IX_hospital_history_2 ON hospital_history (analysis_run_id);
|
||||||
|
|
||||||
|
|
||||||
|
-- market_analysis
|
||||||
|
CREATE TABLE market_analysis
|
||||||
|
(
|
||||||
|
`id` INT NOT NULL AUTO_INCREMENT,
|
||||||
|
`analysis_run_id` CHAR(36) NOT NULL,
|
||||||
|
`analysis_type` VARCHAR(50) NOT NULL,
|
||||||
|
`status` VARCHAR(20) NOT NULL DEFAULT 'start',
|
||||||
|
`data` JSON NULL,
|
||||||
|
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
UNIQUE KEY UQ_market_analysis (analysis_run_id, analysis_type)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IX_market_analysis_1 ON market_analysis (analysis_run_id);
|
||||||
|
|
@ -0,0 +1,236 @@
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import aiomysql
|
||||||
|
from common.utils import get_env
|
||||||
|
|
||||||
|
_pool: aiomysql.Pool | None = None
|
||||||
|
|
||||||
|
|
||||||
|
async def get_pool() -> aiomysql.Pool:
|
||||||
|
global _pool
|
||||||
|
if _pool is None:
|
||||||
|
_pool = await aiomysql.create_pool(
|
||||||
|
host=get_env("MYSQL_HOST"),
|
||||||
|
port=int(os.getenv("MYSQL_PORT", "3306")),
|
||||||
|
user=get_env("MYSQL_USER"),
|
||||||
|
password=get_env("MYSQL_PASSWORD"),
|
||||||
|
db=get_env("MYSQL_DB"),
|
||||||
|
charset="utf8mb4",
|
||||||
|
minsize=0,
|
||||||
|
maxsize=30,
|
||||||
|
connect_timeout=10,
|
||||||
|
)
|
||||||
|
return _pool
|
||||||
|
|
||||||
|
|
||||||
|
async def execute(sql: str, args: tuple = ()) -> int:
|
||||||
|
pool = await get_pool()
|
||||||
|
async with pool.acquire() as conn:
|
||||||
|
try:
|
||||||
|
async with conn.cursor() as cur:
|
||||||
|
await cur.execute(sql, args)
|
||||||
|
await conn.commit()
|
||||||
|
return cur.lastrowid
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
async def fetchone(sql: str, args: tuple = ()) -> dict | None:
|
||||||
|
pool = await get_pool()
|
||||||
|
async with pool.acquire() as conn:
|
||||||
|
try:
|
||||||
|
async with conn.cursor(aiomysql.DictCursor) as cur:
|
||||||
|
await cur.execute(sql, args)
|
||||||
|
return await cur.fetchone()
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
async def fetchall(sql: str, args: tuple = ()) -> list[dict]:
|
||||||
|
pool = await get_pool()
|
||||||
|
async with pool.acquire() as conn:
|
||||||
|
try:
|
||||||
|
async with conn.cursor(aiomysql.DictCursor) as cur:
|
||||||
|
await cur.execute(sql, args)
|
||||||
|
return await cur.fetchall()
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
# ── remote_source ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
async def insert_source(hospital_id: str, source_type: str, url: str) -> int:
|
||||||
|
return await execute(
|
||||||
|
"INSERT INTO remote_source (hospital_id, source_type, url) VALUES (%s, %s, %s)",
|
||||||
|
(hospital_id, source_type, url),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ── raw_info ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
async def insert_raw_info(source_id: int, analysis_run_id: str, data_tag: str = "default") -> int:
|
||||||
|
return await execute(
|
||||||
|
"INSERT INTO raw_info (source_id, analysis_run_id, data_tag) VALUES (%s, %s, %s)",
|
||||||
|
(source_id, analysis_run_id, data_tag),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def set_raw_info_status(info_id: int, status: str) -> None:
|
||||||
|
await execute("UPDATE raw_info SET status = %s WHERE info_id = %s", (status, info_id))
|
||||||
|
|
||||||
|
|
||||||
|
async def save_raw_info(info_id: int, data: dict) -> None:
|
||||||
|
await execute(
|
||||||
|
"UPDATE raw_info SET raw_data = %s, status = 'done' WHERE info_id = %s",
|
||||||
|
(json.dumps(data, ensure_ascii=False), info_id),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def fetch_raw(info_id: int | None) -> dict | None:
|
||||||
|
if info_id is None:
|
||||||
|
return None
|
||||||
|
row = await fetchone("SELECT raw_data FROM raw_info WHERE info_id = %s", (info_id,))
|
||||||
|
if not row or not row["raw_data"]:
|
||||||
|
return None
|
||||||
|
return json.loads(row["raw_data"]) if isinstance(row["raw_data"], str) else row["raw_data"]
|
||||||
|
|
||||||
|
|
||||||
|
async def is_done(info_id: int | None) -> bool:
|
||||||
|
if info_id is None:
|
||||||
|
return True
|
||||||
|
r = await fetchone("SELECT status FROM raw_info WHERE info_id = %s", (info_id,))
|
||||||
|
return r["status"] == "done"
|
||||||
|
|
||||||
|
|
||||||
|
async def get_analysis_raw_data(analysis_run_id: str) -> dict:
|
||||||
|
rows = await fetchall(
|
||||||
|
"SELECT rs.source_type, ri.raw_data"
|
||||||
|
" FROM raw_info ri JOIN remote_source rs USING (source_id)"
|
||||||
|
" WHERE ri.analysis_run_id = %s",
|
||||||
|
(analysis_run_id,),
|
||||||
|
)
|
||||||
|
result: dict = {}
|
||||||
|
for row in rows:
|
||||||
|
raw = row["raw_data"]
|
||||||
|
result[row["source_type"]] = json.loads(raw) if isinstance(raw, str) else raw
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
# ── analysis_runs ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
async def insert_analysis_run(
|
||||||
|
analysis_run_id: str,
|
||||||
|
hospital_id: str,
|
||||||
|
owner_user_id: int,
|
||||||
|
) -> str:
|
||||||
|
await execute(
|
||||||
|
"INSERT INTO analysis_runs (analysis_run_id, hospital_id, owner_user_id)"
|
||||||
|
" VALUES (%s, %s, %s)",
|
||||||
|
(analysis_run_id, hospital_id, owner_user_id),
|
||||||
|
)
|
||||||
|
return analysis_run_id
|
||||||
|
|
||||||
|
|
||||||
|
async def save_analysis_report(analysis_run_id: str, data: dict) -> None:
|
||||||
|
await execute(
|
||||||
|
"UPDATE analysis_runs SET report_data = %s WHERE analysis_run_id = %s",
|
||||||
|
(json.dumps(data, ensure_ascii=False), analysis_run_id),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ── hospital_baseinfo ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
async def _insert_hospital_history(hospital_id: str, analysis_run_id: str | None) -> None:
|
||||||
|
row = await fetchone(
|
||||||
|
"SELECT owner_user_id, hospital_name, hospital_name_en, brn, road_address, site_address, status"
|
||||||
|
" FROM hospital_baseinfo WHERE hospital_id = %s",
|
||||||
|
(hospital_id,),
|
||||||
|
)
|
||||||
|
if not row:
|
||||||
|
return
|
||||||
|
await execute(
|
||||||
|
"INSERT INTO hospital_history"
|
||||||
|
" (hospital_id, owner_user_id, hospital_name, hospital_name_en, brn, road_address, site_address, status, analysis_run_id)"
|
||||||
|
" VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)",
|
||||||
|
(
|
||||||
|
hospital_id,
|
||||||
|
row["owner_user_id"],
|
||||||
|
row["hospital_name"],
|
||||||
|
row["hospital_name_en"],
|
||||||
|
row["brn"],
|
||||||
|
row["road_address"],
|
||||||
|
row["site_address"],
|
||||||
|
row["status"],
|
||||||
|
analysis_run_id,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def insert_hospital(
|
||||||
|
hospital_id: str,
|
||||||
|
name: str,
|
||||||
|
name_en: str | None = None,
|
||||||
|
road_address: str | None = None,
|
||||||
|
site_address: str | None = None,
|
||||||
|
owner_user_id: int = 0,
|
||||||
|
brn: str = "",
|
||||||
|
) -> dict:
|
||||||
|
await execute(
|
||||||
|
"INSERT INTO hospital_baseinfo (hospital_id, hospital_name, hospital_name_en, road_address, site_address, status, owner_user_id, brn)"
|
||||||
|
" VALUES (%s, %s, %s, %s, %s, 'done', %s, %s)",
|
||||||
|
(hospital_id, name, name_en, road_address, site_address, owner_user_id, brn),
|
||||||
|
)
|
||||||
|
await _insert_hospital_history(hospital_id, analysis_run_id=None)
|
||||||
|
return await fetchone(
|
||||||
|
"SELECT created_at FROM hospital_baseinfo WHERE hospital_id = %s",
|
||||||
|
(hospital_id,),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def update_hospital_info(hospital_id: str, data: dict, analysis_run_id: str | None = None) -> None:
|
||||||
|
"""clinic 스크래핑 후 hospital_baseinfo의 기본 필드 업데이트."""
|
||||||
|
await execute(
|
||||||
|
"UPDATE hospital_baseinfo"
|
||||||
|
" SET status = 'done',"
|
||||||
|
" hospital_name = COALESCE(%s, hospital_name),"
|
||||||
|
" hospital_name_en = COALESCE(%s, hospital_name_en),"
|
||||||
|
" road_address = COALESCE(%s, road_address)"
|
||||||
|
" WHERE hospital_id = %s",
|
||||||
|
(
|
||||||
|
data.get("clinicName"),
|
||||||
|
data.get("clinicNameEn"),
|
||||||
|
data.get("address"),
|
||||||
|
hospital_id,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
await _insert_hospital_history(hospital_id, analysis_run_id)
|
||||||
|
|
||||||
|
|
||||||
|
# ── file_data ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
async def insert_file_row(
|
||||||
|
analysis_run_id: str,
|
||||||
|
file_type: str,
|
||||||
|
file_name: str,
|
||||||
|
file_url: str,
|
||||||
|
size_bytes: int | None = None,
|
||||||
|
hospital_id: str | None = None,
|
||||||
|
) -> int:
|
||||||
|
return await execute(
|
||||||
|
"INSERT INTO file_data (analysis_run_id, hospital_id, file_type, file_name, file_url, size_bytes)"
|
||||||
|
" VALUES (%s, %s, %s, %s, %s, %s)",
|
||||||
|
(analysis_run_id, hospital_id, file_type, file_name, file_url, size_bytes),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ── market_analysis ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
async def get_market_analysis(analysis_run_id: str) -> dict:
|
||||||
|
rows = await fetchall(
|
||||||
|
"SELECT analysis_type, data FROM market_analysis WHERE analysis_run_id = %s AND status = 'done'",
|
||||||
|
(analysis_run_id,),
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
row["analysis_type"]: json.loads(row["data"]) if isinstance(row["data"], str) else row["data"]
|
||||||
|
for row in rows
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue