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 @@
- | 프리덱 |
+ 프리덱 |
+ 가로보 |
+
+
| 일반부(㎡) |
중분대(㎡) |
방호벽부(㎡) |
비고 |
- 가로보 |
형고(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 || '-')} |
-
+ ${crossbeamRows.map((row, index) => `
+
+ ${index === 0 ? `
+ | ${escapeHtml(predeck.generalArea || '-')} |
+ ${escapeHtml(predeck.medianArea || '-')} |
+ ${escapeHtml(predeck.barrierArea || '-')} |
+ ${escapeHtml(predeck.remarks || '-')} |
+ ` : ''}
+ ${escapeHtml(row.height || '-')} |
+ ${escapeHtml(row.length || '-')} |
+ ${escapeHtml(row.quantity || '-')} |
+ ${escapeHtml(row.remarks || '-')} |
+
+ `).join('')}
`;
@@ -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 @@
교량제원 - 프리덱 / 가로보
- | 프리덱 일반부 | ${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) | 비고 |
+ ${crossbeamRows.map((row, index) => `
+
+ ${index === 0 ? `| 일반부(㎡) | ${escapeHtml(predeck.generalArea || '-')} | ` : ''}
+ ${index === 1 ? `중분대(㎡) | ${escapeHtml(predeck.medianArea || '-')} | ` : ''}
+ ${index === 2 ? `방호벽부(㎡) | ${escapeHtml(predeck.barrierArea || '-')} | ` : ''}
+ ${index >= 3 ? `비고 | ${escapeHtml(predeck.remarks || '-')} | ` : ''}
+ ${escapeHtml(row.height || '-')} |
+ ${escapeHtml(row.length || '-')} |
+ ${escapeHtml(row.quantity || '-')} |
+ ${escapeHtml(row.remarks || '-')} |
+
+ `).join('')}
+ ${crossbeamRows.length < 4 ? `
+ ${crossbeamRows.length <= 0 ? '' : ''}
+ ${crossbeamRows.length < 2 ? `| 중분대(㎡) | ${escapeHtml(predeck.medianArea || '-')} | |
` : ''}
+ ${crossbeamRows.length < 3 ? `| 방호벽부(㎡) | ${escapeHtml(predeck.barrierArea || '-')} | |
` : ''}
+ ${crossbeamRows.length < 4 ? `| 비고 | ${escapeHtml(predeck.remarks || '-')} | |
` : ''}
+ ` : ''}
`;
}