From 62b25b045b67e40994865889bade671c48249e51 Mon Sep 17 00:00:00 2001 From: Hyein Date: Tue, 9 Jun 2026 13:56:35 +0900 Subject: [PATCH] Show all 8092 crossbeam rows --- mysql_preview_server.py | 109 ++++++++++++++++++++++++---------------- project-codes.html | 67 +++++++++++++++++------- 2 files changed, 116 insertions(+), 60 deletions(-) diff --git a/mysql_preview_server.py b/mysql_preview_server.py index 746435c..a9079f3 100644 --- a/mysql_preview_server.py +++ b/mysql_preview_server.py @@ -50,6 +50,7 @@ SITE_SYNC_JOBS = {} SITE_SYNC_LOCK = threading.Lock() DB_INIT_LOCK = threading.Lock() DB_SCHEMA_READY = False +DB_WRITE_LOCK = threading.Lock() def configure_sqlite_connection(conn): @@ -64,6 +65,24 @@ def open_db_connection(timeout=30): return configure_sqlite_connection(conn) +def run_db_write(write_fn, retries=4, delay=0.2): + last_error = None + for attempt in range(retries): + try: + with DB_WRITE_LOCK: + with open_db_connection() as conn: + ensure_db_schema(conn) + return write_fn(conn) + except sqlite3.OperationalError as error: + last_error = error + if 'locked' not in str(error).lower() or attempt == retries - 1: + raise + time.sleep(delay * (attempt + 1)) + if last_error: + raise last_error + raise RuntimeError('DB write failed') + + def init_db(conn): global DB_SCHEMA_READY @@ -1152,16 +1171,22 @@ def _parse_budget_predeck(rows): def _parse_budget_crossbeam(rows): - result = {'height': '', 'length': '', 'quantity': '', 'remarks': ''} + result = [] if len(rows) < 2: return result - values = rows[1] + [''] * max(0, 4 - len(rows[1])) - result.update({ - 'height': _as_text(values[0]).strip(), - 'length': _as_text(values[1]).strip(), - 'quantity': _as_text(values[2]).strip(), - 'remarks': _as_text(values[3]).strip(), - }) + for row in rows[1:]: + if not row: + continue + first = _as_text(row[0]).strip() + if not first or '조회된 내용이 없습니다' in first: + continue + values = row + [''] * max(0, 4 - len(row)) + result.append({ + 'height': _as_text(values[0]).strip(), + 'length': _as_text(values[1]).strip(), + 'quantity': _as_text(values[2]).strip(), + 'remarks': _as_text(values[3]).strip(), + }) return result @@ -4004,25 +4029,13 @@ class Handler(BaseHTTPRequestHandler): q = parse_qs(parsed.query) page_name = q.get('page', ['const'])[0] refresh = q.get('refresh', ['0'])[0] in ('1', 'true', 'yes') - with open_db_connection() as conn: - ensure_db_schema(conn) - if refresh: - search_text = q.get('searchText', [''])[0] - result = fetch_erp_project_codes(page_name, search_text) - sync_info = replace_erp_project_code_cache(conn, result['page'], result['rows']) + if refresh: + search_text = q.get('searchText', [''])[0] + result = fetch_erp_project_codes(page_name, search_text) + sync_info = run_db_write(lambda conn: replace_erp_project_code_cache(conn, result['page'], result['rows'])) + with open_db_connection() as conn: + ensure_db_schema(conn) cached = get_erp_project_code_cache(conn, result['page']) - return self._json( - 200, - { - 'count': len(cached['rows']), - 'rows': cached['rows'], - 'source': 'erp_cache', - 'page': cached['sourcePage'], - 'syncedAt': sync_info['syncedAt'], - }, - ) - - cached = get_erp_project_code_cache(conn, page_name) return self._json( 200, { @@ -4030,21 +4043,35 @@ class Handler(BaseHTTPRequestHandler): 'rows': cached['rows'], 'source': 'erp_cache', 'page': cached['sourcePage'], - 'syncedAt': cached['syncedAt'], + 'syncedAt': sync_info['syncedAt'], }, ) + with open_db_connection() as conn: + ensure_db_schema(conn) + cached = get_erp_project_code_cache(conn, page_name) + return self._json( + 200, + { + 'count': len(cached['rows']), + 'rows': cached['rows'], + 'source': 'erp_cache', + 'page': cached['sourcePage'], + 'syncedAt': cached['syncedAt'], + }, + ) + if parsed.path == '/api/erp-contract-detail': q = parse_qs(parsed.query) page_name = q.get('page', ['const'])[0] project_code = q.get('projectCode', [''])[0] project_name = q.get('projectName', [''])[0] refresh = q.get('refresh', ['0'])[0] in ('1', 'true', 'yes') + if refresh: + detail = fetch_erp_contract_detail(page_name, project_code, project_name) + run_db_write(lambda conn: replace_erp_contract_detail_cache(conn, detail)) with open_db_connection() as conn: ensure_db_schema(conn) - if refresh: - detail = fetch_erp_contract_detail(page_name, project_code, project_name) - replace_erp_contract_detail_cache(conn, detail) cached = get_erp_contract_detail_cache(conn, page_name, project_code) if not cached: return self._json(404, {'ok': False, 'error': '계약정보 캐시가 없습니다.'}) @@ -4062,11 +4089,11 @@ class Handler(BaseHTTPRequestHandler): project_code = q.get('projectCode', [''])[0] project_name = q.get('projectName', [''])[0] refresh = q.get('refresh', ['0'])[0] in ('1', 'true', 'yes') + if refresh: + result = fetch_erp_bridge_overviews(page_name, project_code, project_name) + run_db_write(lambda conn: replace_erp_bridge_overview_cache(conn, result)) with open_db_connection() as conn: ensure_db_schema(conn) - if refresh: - result = fetch_erp_bridge_overviews(page_name, project_code, project_name) - replace_erp_bridge_overview_cache(conn, result) cached = get_erp_bridge_overview_cache(conn, page_name, project_code) return self._json( 200, @@ -4083,11 +4110,11 @@ class Handler(BaseHTTPRequestHandler): project_code = q.get('projectCode', [''])[0] project_name = q.get('projectName', [''])[0] refresh = q.get('refresh', ['0'])[0] in ('1', 'true', 'yes') + if refresh: + result = fetch_erp_budget_plan(page_name, project_code, project_name) + run_db_write(lambda conn: replace_erp_budget_plan_cache(conn, result)) with open_db_connection() as conn: ensure_db_schema(conn) - if refresh: - result = fetch_erp_budget_plan(page_name, project_code, project_name) - replace_erp_budget_plan_cache(conn, result) cached = get_erp_budget_plan_cache(conn, page_name, project_code) if not cached: return self._json(404, {'ok': False, 'error': '공사시행계획서 캐시가 없습니다.'}) @@ -4271,9 +4298,7 @@ class Handler(BaseHTTPRequestHandler): q = parse_qs(parsed.query) page_name = q.get('page', ['const'])[0] result = fetch_erp_project_codes(page_name) - with open_db_connection() as conn: - ensure_db_schema(conn) - sync_info = replace_erp_project_code_cache(conn, result['page'], result['rows']) + sync_info = run_db_write(lambda conn: replace_erp_project_code_cache(conn, result['page'], result['rows'])) return self._json( 200, { @@ -4294,9 +4319,9 @@ class Handler(BaseHTTPRequestHandler): project_code = q.get('projectCode', [''])[0] project_name = q.get('projectName', [''])[0] detail = fetch_erp_contract_detail(page_name, project_code, project_name) + sync_info = run_db_write(lambda conn: replace_erp_contract_detail_cache(conn, detail)) with open_db_connection() as conn: ensure_db_schema(conn) - sync_info = replace_erp_contract_detail_cache(conn, detail) cached = get_erp_contract_detail_cache(conn, page_name, project_code) return self._json( 200, @@ -4317,9 +4342,9 @@ class Handler(BaseHTTPRequestHandler): project_code = q.get('projectCode', [''])[0] project_name = q.get('projectName', [''])[0] result = fetch_erp_bridge_overviews(page_name, project_code, project_name) + sync_info = run_db_write(lambda conn: replace_erp_bridge_overview_cache(conn, result)) with open_db_connection() as conn: ensure_db_schema(conn) - sync_info = replace_erp_bridge_overview_cache(conn, result) cached = get_erp_bridge_overview_cache(conn, page_name, project_code) return self._json( 200, @@ -4341,9 +4366,9 @@ class Handler(BaseHTTPRequestHandler): project_code = q.get('projectCode', [''])[0] project_name = q.get('projectName', [''])[0] result = fetch_erp_budget_plan(page_name, project_code, project_name) + sync_info = run_db_write(lambda conn: replace_erp_budget_plan_cache(conn, result)) with open_db_connection() as conn: ensure_db_schema(conn) - sync_info = replace_erp_budget_plan_cache(conn, result) cached = get_erp_budget_plan_cache(conn, page_name, project_code) return self._json(200, {'ok': True, 'syncedAt': sync_info['syncedAt'], 'plan': cached}) except Exception as e: diff --git a/project-codes.html b/project-codes.html index 3bb6563..916ef4e 100644 --- a/project-codes.html +++ b/project-codes.html @@ -669,13 +669,23 @@ .replace(/(?<=\d)-(?=\d)/g, '-\n'); } + function normalizeCrossbeamRows(crossbeam) { + if (Array.isArray(crossbeam)) { + return crossbeam.length ? crossbeam : [{ height: '', length: '', quantity: '', remarks: '' }]; + } + if (crossbeam && typeof crossbeam === 'object') { + return [crossbeam]; + } + return [{ height: '', length: '', quantity: '', remarks: '' }]; + } + function renderBudgetPlanInlineTables(plan) { if (!plan) return '-'; const inputDays = plan.inputDays || {}; const inputRebar = plan.inputRebar || {}; const girderSpecs = Array.isArray(plan.girderSpecs) ? plan.girderSpecs : []; const predeck = plan.predeck || {}; - const crossbeam = plan.crossbeam || {}; + const crossbeamRows = normalizeCrossbeamRows(plan.crossbeam); return `
@@ -739,27 +749,33 @@ - + + + + - - - - - - - - - - - + ${crossbeamRows.map((row, index) => ` + + ${index === 0 ? ` + + + + + ` : ''} + + + + + + `).join('')}
프리덱프리덱가로보
일반부(㎡) 중분대(㎡) 방호벽부(㎡) 비고가로보 형고(m) 길이(m) 수량(EA) 비고
${escapeHtml(predeck.generalArea || '-')}${escapeHtml(predeck.medianArea || '-')}${escapeHtml(predeck.barrierArea || '-')}${escapeHtml(predeck.remarks || '-')}${escapeHtml(crossbeam.height || '-')}${escapeHtml(crossbeam.length || '-')}${escapeHtml(crossbeam.quantity || '-')}${escapeHtml(crossbeam.remarks || '-')}
${escapeHtml(predeck.generalArea || '-')}${escapeHtml(predeck.medianArea || '-')}${escapeHtml(predeck.barrierArea || '-')}${escapeHtml(predeck.remarks || '-')}${escapeHtml(row.height || '-')}${escapeHtml(row.length || '-')}${escapeHtml(row.quantity || '-')}${escapeHtml(row.remarks || '-')}
`; @@ -920,7 +936,7 @@ const inputRebar = plan.inputRebar || {}; const girderSpecs = Array.isArray(plan.girderSpecs) ? plan.girderSpecs : []; const predeck = plan.predeck || {}; - const crossbeam = plan.crossbeam || {}; + const crossbeamRows = normalizeCrossbeamRows(plan.crossbeam); planModalTitle.textContent = `${bridgeName || plan.projectName || '프로젝트'} 공사시행계획서`; planModalBody.innerHTML = ` @@ -948,10 +964,25 @@
교량제원 - 프리덱 / 가로보
- - - - + + ${crossbeamRows.map((row, index) => ` + + ${index === 0 ? `` : ''} + ${index === 1 ? `` : ''} + ${index === 2 ? `` : ''} + ${index >= 3 ? `` : ''} + + + + + + `).join('')} + ${crossbeamRows.length < 4 ? ` + ${crossbeamRows.length <= 0 ? '' : ''} + ${crossbeamRows.length < 2 ? `` : ''} + ${crossbeamRows.length < 3 ? `` : ''} + ${crossbeamRows.length < 4 ? `` : ''} + ` : ''}
프리덱 일반부${escapeHtml(predeck.generalArea || '-')}가로보 형고${escapeHtml(crossbeam.height || '-')}
프리덱 중분대${escapeHtml(predeck.medianArea || '-')}가로보 길이${escapeHtml(crossbeam.length || '-')}
프리덱 방호벽부${escapeHtml(predeck.barrierArea || '-')}가로보 수량${escapeHtml(crossbeam.quantity || '-')}
프리덱 비고${escapeHtml(predeck.remarks || '-')}가로보 비고${escapeHtml(crossbeam.remarks || '-')}
프리덱형고(m)길이(m)수량(EA)비고
일반부(㎡)${escapeHtml(predeck.generalArea || '-')}중분대(㎡)${escapeHtml(predeck.medianArea || '-')}방호벽부(㎡)${escapeHtml(predeck.barrierArea || '-')}비고${escapeHtml(predeck.remarks || '-')}${escapeHtml(row.height || '-')}${escapeHtml(row.length || '-')}${escapeHtml(row.quantity || '-')}${escapeHtml(row.remarks || '-')}
중분대(㎡)${escapeHtml(predeck.medianArea || '-')}
방호벽부(㎡)${escapeHtml(predeck.barrierArea || '-')}
비고${escapeHtml(predeck.remarks || '-')}
`; }