diff --git a/mysql_preview_server.py b/mysql_preview_server.py
index 6004fa6..96c6583 100644
--- a/mysql_preview_server.py
+++ b/mysql_preview_server.py
@@ -12,6 +12,7 @@ import html as html_lib
import threading
import uuid
import subprocess
+from pathlib import Path
from datetime import datetime, timedelta, time
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from urllib.parse import parse_qs, quote, urlparse
@@ -140,6 +141,14 @@ def init_db(conn):
shortName TEXT
);
+ CREATE TABLE IF NOT EXISTS erp_project_alias_cache (
+ sourcePage TEXT NOT NULL,
+ projectCode TEXT NOT NULL,
+ shortName TEXT NOT NULL DEFAULT '',
+ syncedAt TEXT NOT NULL,
+ PRIMARY KEY (sourcePage, projectCode)
+ );
+
CREATE TABLE IF NOT EXISTS site_worksheet_record (
projectCode TEXT,
workDate TEXT,
@@ -288,6 +297,20 @@ def init_db(conn):
PRIMARY KEY (sourcePage, projectCode)
);
+ CREATE TABLE IF NOT EXISTS erp_linked_code_cache (
+ sourcePage TEXT NOT NULL,
+ projectCode TEXT NOT NULL,
+ projectName TEXT NOT NULL DEFAULT '',
+ businessCode TEXT DEFAULT '',
+ salesCode TEXT DEFAULT '',
+ salesName TEXT DEFAULT '',
+ designCode TEXT DEFAULT '',
+ designName TEXT DEFAULT '',
+ matchedBy TEXT DEFAULT '',
+ syncedAt TEXT NOT NULL,
+ PRIMARY KEY (sourcePage, projectCode)
+ );
+
CREATE TABLE IF NOT EXISTS naver_address_token_cache (
address TEXT PRIMARY KEY,
tokenX TEXT NOT NULL,
@@ -328,6 +351,7 @@ def init_db(conn):
conn.execute("ALTER TABLE dallyproject ADD COLUMN BusinessTripHours REAL DEFAULT 0")
conn.execute("CREATE INDEX IF NOT EXISTS idx_dally_workdate ON dallyproject(WorkDate)")
conn.execute("CREATE INDEX IF NOT EXISTS idx_project_alias_code ON project_alias(projectCode)")
+ conn.execute("CREATE INDEX IF NOT EXISTS idx_erp_project_alias_cache_page ON erp_project_alias_cache(sourcePage, projectCode)")
conn.execute("CREATE INDEX IF NOT EXISTS idx_site_worksheet_member ON site_worksheet_record(memberNo, workDate)")
conn.execute("CREATE INDEX IF NOT EXISTS idx_site_worksheet_name ON site_worksheet_record(korName, workDate)")
conn.execute("CREATE INDEX IF NOT EXISTS idx_site_worksheet_sync_member ON site_worksheet_sync(memberNo, yearMonth)")
@@ -340,6 +364,7 @@ def init_db(conn):
conn.execute("CREATE INDEX IF NOT EXISTS idx_erp_contract_detail_cache_page ON erp_contract_detail_cache(sourcePage, projectCode)")
conn.execute("CREATE INDEX IF NOT EXISTS idx_erp_bridge_overview_cache_page ON erp_bridge_overview_cache(sourcePage, projectCode, bridgeNo)")
conn.execute("CREATE INDEX IF NOT EXISTS idx_erp_budget_plan_cache_page ON erp_budget_plan_cache(sourcePage, projectCode)")
+ conn.execute("CREATE INDEX IF NOT EXISTS idx_erp_linked_code_cache_page ON erp_linked_code_cache(sourcePage, projectCode)")
conn.commit()
DB_SCHEMA_READY = True
@@ -853,7 +878,7 @@ def backfill_daily_hours_if_needed(conn):
return need
-def load_project_alias_from_erp():
+def load_project_alias_entries_from_erp():
login_url = ERP_BASE_URL.rstrip('/') + '/sys/controller/common/main_controller.php'
s = requests.Session()
login_payload = {
@@ -886,8 +911,12 @@ def load_project_alias_from_erp():
code = _as_text(item.get('ProjectCode')).strip().upper()
nick = _as_text(item.get('ProjectNickname')).strip()
if code and nick:
- out[code] = nick
- return sorted(out.items())
+ out[code] = {'sourcePage': page, 'projectCode': code, 'shortName': nick}
+ return sorted(out.values(), key=lambda item: (item['projectCode'], item['sourcePage']))
+
+
+def load_project_alias_from_erp():
+ return [(item['projectCode'], item['shortName']) for item in load_project_alias_entries_from_erp()]
def erp_login_session():
@@ -1679,6 +1708,7 @@ def fetch_erp_contract_detail(page='const', project_code='', project_name=''):
'finalContractAmountText': contract_map.get('최종계약금액', ''),
'finalContractAmountValue': _parse_amount_text(contract_map.get('최종계약금액', '')),
'contractType': contract_map.get('계약종류', ''),
+ 'linkedCodes': find_erp_linked_codes(code, contract_map.get('사업코드', ''), short_name or contract_map.get('약칭', '')),
'contractFields': contract_map,
'scaleRows': scale_rows,
}
@@ -1695,6 +1725,7 @@ def replace_erp_contract_detail_cache(conn, detail):
raw_payload = {
'contractFields': contract_fields,
'scaleRows': detail.get('scaleRows') or [],
+ 'linkedCodes': detail.get('linkedCodes') or {},
}
conn.execute(
@@ -1743,9 +1774,11 @@ def get_erp_contract_detail_cache(conn, source_page='const', project_code=''):
if isinstance(raw_contract, dict) and ('contractFields' in raw_contract or 'scaleRows' in raw_contract):
contract_fields = raw_contract.get('contractFields') or {}
scale_rows = raw_contract.get('scaleRows') or []
+ linked_codes = _decorate_linked_code_names(raw_contract.get('linkedCodes') or {}, conn)
else:
contract_fields = raw_contract if isinstance(raw_contract, dict) else {}
scale_rows = []
+ linked_codes = {}
return {
'sourcePage': source_page,
'projectCode': project_code,
@@ -1757,6 +1790,7 @@ def get_erp_contract_detail_cache(conn, source_page='const', project_code=''):
'finalContractAmountText': row[5] or '',
'finalContractAmountValue': row[6] or 0,
'contractType': row[7] or '',
+ 'linkedCodes': linked_codes,
'contractFields': contract_fields,
'scaleRows': scale_rows,
'syncedAt': row[9] or '',
@@ -1768,6 +1802,156 @@ def _clean_html_text(value):
return re.sub(r'\s+', ' ', text).strip()
+def _normalize_text_key(value):
+ return re.sub(r'\s+', '', _as_text(value)).strip()
+
+
+def _extract_business_code_key(value):
+ text = _as_text(value).strip()
+ match = re.search(r'\d{2}-\d{3}', text)
+ return match.group(0) if match else text
+
+
+def _load_linked_code_map_from_html():
+ html_path = Path(__file__).with_name('project-codes.html')
+ if not html_path.exists():
+ return {}
+ try:
+ html_text = html_path.read_text(encoding='utf-8')
+ except OSError:
+ return {}
+ match = re.search(r'const\s+LINKED_CODE_BY_BUSINESS\s*=\s*(\{.*?\});', html_text, re.S)
+ if not match:
+ return {}
+ try:
+ parsed = json.loads(match.group(1))
+ except Exception:
+ return {}
+ return parsed if isinstance(parsed, dict) else {}
+
+
+def _decorate_linked_code_names(linked_codes, conn=None):
+ payload = dict(linked_codes or {})
+ close_conn = False
+ if conn is None:
+ conn = open_db_connection()
+ close_conn = True
+ try:
+ sales_code = _as_text(payload.get('salesCode')).strip()
+ design_code = _as_text(payload.get('designCode')).strip()
+ if sales_code and not _as_text(payload.get('salesName')).strip():
+ payload['salesName'] = get_project_alias_name(conn, sales_code, 'sales')
+ if design_code and not _as_text(payload.get('designName')).strip():
+ payload['designName'] = get_project_alias_name(conn, design_code, 'design')
+ return payload
+ finally:
+ if close_conn:
+ conn.close()
+
+
+def fetch_erp_linked_code_rows():
+ session = erp_login_session()
+ report_url = ERP_BASE_URL.rstrip('/') + '/sys/controller/report/design_step_controller.php'
+ response = session.get(report_url, params={'ActionMode': 'REPORT_5'}, timeout=20)
+ html = response.content.decode('utf-8', errors='ignore')
+ tables = _extract_table_rows(html)
+ target_rows = []
+ for rows in tables:
+ if not rows:
+ continue
+ header = rows[0]
+ if '사업코드' in header and '영업코드' in header and '설계코드' in header and '시공코드' in header:
+ target_rows = rows
+ break
+ if not target_rows:
+ return []
+
+ header = target_rows[0]
+ header_index = {label: index for index, label in enumerate(header)}
+ business_index = header_index.get('사업코드')
+ bridge_name_index = header_index.get('교량명')
+ sales_index = header_index.get('영업코드')
+ design_index = header_index.get('설계코드')
+ const_index = header_index.get('시공코드')
+ status_index = header_index.get('진행상태')
+
+ out = []
+ for row in target_rows[1:]:
+ if not row:
+ continue
+ if business_index is None or business_index >= len(row):
+ continue
+ business_code = _as_text(row[business_index]).strip()
+ if not business_code or not re.match(r'^\d{2}-\d{3}', business_code):
+ continue
+ out.append({
+ 'businessCode': business_code,
+ 'bridgeName': _as_text(row[bridge_name_index]).strip() if bridge_name_index is not None and bridge_name_index < len(row) else '',
+ 'status': _as_text(row[status_index]).strip() if status_index is not None and status_index < len(row) else '',
+ 'salesCode': _as_text(row[sales_index]).strip() if sales_index is not None and sales_index < len(row) else '',
+ 'designCode': _as_text(row[design_index]).strip() if design_index is not None and design_index < len(row) else '',
+ 'constCode': _as_text(row[const_index]).strip() if const_index is not None and const_index < len(row) else '',
+ })
+ return out
+
+
+def find_erp_linked_codes(project_code='', business_code='', project_name=''):
+ project_code = _as_text(project_code).strip()
+ business_code = _extract_business_code_key(business_code)
+ project_name_key = _normalize_text_key(project_name)
+ html_map = _load_linked_code_map_from_html()
+
+ def html_map_match():
+ if project_code:
+ for key, value in html_map.items():
+ if _as_text((value or {}).get('constCode')).strip() == project_code:
+ return _decorate_linked_code_names({
+ 'businessCode': key,
+ 'bridgeName': '',
+ 'status': '',
+ 'salesCode': _as_text((value or {}).get('salesCode')).strip(),
+ 'designCode': _as_text((value or {}).get('designCode')).strip(),
+ 'constCode': _as_text((value or {}).get('constCode')).strip(),
+ 'matchedBy': 'htmlMap.constCode',
+ })
+ if business_code and business_code in html_map:
+ value = html_map.get(business_code) or {}
+ return _decorate_linked_code_names({
+ 'businessCode': business_code,
+ 'bridgeName': '',
+ 'status': '',
+ 'salesCode': _as_text(value.get('salesCode')).strip(),
+ 'designCode': _as_text(value.get('designCode')).strip(),
+ 'constCode': _as_text(value.get('constCode')).strip(),
+ 'matchedBy': 'htmlMap.businessCode',
+ })
+ return None
+
+ html_hit = html_map_match()
+ if html_hit:
+ return html_hit
+
+ rows = fetch_erp_linked_code_rows()
+ if not rows:
+ return {'salesCode': '', 'salesName': '', 'designCode': '', 'designName': '', 'constCode': '', 'matchedBy': ''}
+
+ for row in rows:
+ if project_code and _as_text(row.get('constCode')).strip() == project_code:
+ return _decorate_linked_code_names({**row, 'matchedBy': 'constCode'})
+
+ candidates = [row for row in rows if business_code and _as_text(row.get('businessCode')).strip() == business_code]
+ if len(candidates) == 1:
+ return _decorate_linked_code_names({**candidates[0], 'matchedBy': 'businessCode'})
+ if len(candidates) > 1 and project_name_key:
+ for row in candidates:
+ bridge_name_key = _normalize_text_key(row.get('bridgeName'))
+ if project_name_key and project_name_key in bridge_name_key:
+ return _decorate_linked_code_names({**row, 'matchedBy': 'businessCode+projectName'})
+ return _decorate_linked_code_names({**candidates[0], 'matchedBy': 'businessCode'})
+
+ return {'salesCode': '', 'salesName': '', 'designCode': '', 'designName': '', 'constCode': '', 'matchedBy': ''}
+
+
def _parse_site_worker_rows(html):
rows = []
for table in re.findall(r'
', html or '', re.I | re.S):
@@ -2726,11 +2910,152 @@ def replace_project_alias(conn, rows):
conn.commit()
+def replace_erp_project_alias_cache(conn, rows):
+ synced_at = datetime.now().isoformat(timespec='seconds')
+ conn.execute('DELETE FROM erp_project_alias_cache')
+ if rows:
+ conn.executemany(
+ '''
+ INSERT OR REPLACE INTO erp_project_alias_cache (sourcePage, projectCode, shortName, syncedAt)
+ VALUES (?, ?, ?, ?)
+ ''',
+ [
+ (
+ _as_text(row.get('sourcePage')).strip().lower(),
+ _as_text(row.get('projectCode')).strip().upper(),
+ _as_text(row.get('shortName')).strip(),
+ synced_at,
+ )
+ for row in rows
+ if _as_text(row.get('sourcePage')).strip() and _as_text(row.get('projectCode')).strip()
+ ],
+ )
+ conn.commit()
+ return {'count': len(rows), 'syncedAt': synced_at}
+
+
def get_project_alias_map(conn):
cur = conn.execute('SELECT projectCode, shortName FROM project_alias')
return {code: short for code, short in cur.fetchall() if code}
+def infer_source_page_from_code(project_code=''):
+ code = _as_text(project_code).strip().upper()
+ if '-교영-' in code:
+ return 'sales'
+ if '-설계-' in code:
+ return 'design'
+ if '-시공-' in code:
+ return 'const'
+ if '-제조-' in code:
+ return 'make'
+ if '-연구-' in code:
+ return 'research'
+ return ''
+
+
+def get_project_alias_name(conn, project_code='', source_page=''):
+ code = _as_text(project_code).strip().upper()
+ page_name = _as_text(source_page).strip().lower() or infer_source_page_from_code(code)
+ if not code:
+ return ''
+ if page_name:
+ row = conn.execute(
+ 'SELECT shortName FROM erp_project_alias_cache WHERE sourcePage = ? AND projectCode = ?',
+ (page_name, code),
+ ).fetchone()
+ if row and row[0]:
+ return row[0]
+ row = conn.execute('SELECT shortName FROM project_alias WHERE projectCode = ?', (code,)).fetchone()
+ return row[0] if row and row[0] else ''
+
+
+def replace_erp_linked_code_cache(conn, source_page, project_code, project_name, business_code, linked_codes):
+ source_page = _as_text(source_page).strip().lower() or 'const'
+ project_code = _as_text(project_code).strip()
+ if not project_code:
+ return None
+ payload = linked_codes or {}
+ synced_at = datetime.now().isoformat(timespec='seconds')
+ conn.execute(
+ '''
+ INSERT OR REPLACE INTO erp_linked_code_cache
+ (sourcePage, projectCode, projectName, businessCode, salesCode, salesName, designCode, designName, matchedBy, syncedAt)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+ ''',
+ (
+ source_page,
+ project_code,
+ _as_text(project_name).strip(),
+ _extract_business_code_key(business_code),
+ _as_text(payload.get('salesCode')).strip(),
+ _as_text(payload.get('salesName')).strip(),
+ _as_text(payload.get('designCode')).strip(),
+ _as_text(payload.get('designName')).strip(),
+ _as_text(payload.get('matchedBy')).strip(),
+ synced_at,
+ ),
+ )
+ conn.commit()
+ return {'sourcePage': source_page, 'projectCode': project_code, 'syncedAt': synced_at}
+
+
+def sync_project_alias_caches(conn):
+ alias_entries = load_project_alias_entries_from_erp()
+ replace_project_alias(conn, [(item['projectCode'], item['shortName']) for item in alias_entries])
+ cache_info = replace_erp_project_alias_cache(conn, alias_entries)
+ return {'entries': alias_entries, 'cacheInfo': cache_info}
+
+
+def rebuild_erp_linked_code_cache(conn, source_page='const'):
+ source_page = _as_text(source_page).strip().lower() or 'const'
+ rows = conn.execute(
+ '''
+ SELECT p.projectCode,
+ p.projectName,
+ COALESCE(d.businessCode, '')
+ FROM erp_project_code_cache p
+ LEFT JOIN erp_contract_detail_cache d
+ ON d.sourcePage = p.sourcePage
+ AND d.projectCode = p.projectCode
+ WHERE p.sourcePage = ?
+ ORDER BY p.projectCode ASC
+ ''',
+ (source_page,),
+ ).fetchall()
+ synced_at = datetime.now().isoformat(timespec='seconds')
+ payload_rows = []
+ for project_code, project_name, business_code in rows:
+ linked_codes = find_erp_linked_codes(project_code, business_code, project_name)
+ if linked_codes.get('salesCode') or linked_codes.get('designCode'):
+ payload_rows.append(
+ (
+ source_page,
+ _as_text(project_code).strip(),
+ _as_text(project_name).strip(),
+ _extract_business_code_key(business_code),
+ _as_text(linked_codes.get('salesCode')).strip(),
+ _as_text(linked_codes.get('salesName')).strip(),
+ _as_text(linked_codes.get('designCode')).strip(),
+ _as_text(linked_codes.get('designName')).strip(),
+ _as_text(linked_codes.get('matchedBy')).strip(),
+ synced_at,
+ )
+ )
+ conn.execute('DELETE FROM erp_linked_code_cache WHERE sourcePage = ?', (source_page,))
+ if payload_rows:
+ conn.executemany(
+ '''
+ INSERT OR REPLACE INTO erp_linked_code_cache
+ (sourcePage, projectCode, projectName, businessCode, salesCode, salesName, designCode, designName, matchedBy, syncedAt)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+ ''',
+ payload_rows,
+ )
+ conn.commit()
+ return len(payload_rows)
+
+
def rebuild_work_calendar_tables(conn):
conn.execute('DELETE FROM work_calendar_detail')
conn.execute('DELETE FROM work_calendar_day')
@@ -3382,7 +3707,8 @@ def _duration_sql(alias='d'):
def get_people_summary(conn, start_date, end_date):
- q = f'''
+ params = (start_date, end_date, start_date, end_date)
+ q_with_site = f'''
WITH d AS (
SELECT
MemberNo,
@@ -3417,7 +3743,37 @@ def get_people_summary(conn, start_date, end_date):
LEFT JOIN s ON s.MemberNo = m.MemberNo
ORDER BY CASE WHEN IFNULL(m.korName,'')='' THEN 1 ELSE 0 END, korName ASC, m.MemberNo ASC
'''
- cur = conn.execute(q, (start_date, end_date, start_date, end_date))
+ try:
+ cur = conn.execute(q_with_site, params)
+ except sqlite3.DatabaseError as error:
+ if 'malformed' not in str(error).lower():
+ raise
+ q_without_site = f'''
+ WITH d AS (
+ SELECT
+ MemberNo,
+ ROUND(SUM(TotalHours), 2) AS totalHours,
+ COUNT(*) AS totalRows
+ FROM dallyproject
+ WHERE date(substr(EntryTime,1,10)) BETWEEN date(?) AND date(?)
+ GROUP BY MemberNo
+ )
+ SELECT
+ m.MemberNo,
+ IFNULL(m.korName, '') AS korName,
+ IFNULL(m.rankName, '') AS rankName,
+ IFNULL(m.teamName, '') AS teamName,
+ IFNULL(m.retireFlag, '') AS retireFlag,
+ IFNULL(m.isRetired, 0) AS isRetired,
+ IFNULL(d.totalHours, 0) AS totalHours,
+ IFNULL(d.totalRows, 0) AS totalRows,
+ 0 AS siteHours,
+ 0 AS siteRows
+ FROM member m
+ LEFT JOIN d ON d.MemberNo = m.MemberNo
+ ORDER BY CASE WHEN IFNULL(m.korName,'')='' THEN 1 ELSE 0 END, korName ASC, m.MemberNo ASC
+ '''
+ cur = conn.execute(q_without_site, (start_date, end_date))
cols = [d[0] for d in cur.description]
return [dict(zip(cols, row)) for row in cur.fetchall()]
@@ -4267,10 +4623,49 @@ class Handler(BaseHTTPRequestHandler):
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))
+ def _write_refresh(conn):
+ replace_erp_contract_detail_cache(conn, detail)
+ replace_erp_linked_code_cache(
+ conn,
+ page_name,
+ project_code,
+ detail.get('projectName', '') or project_name,
+ detail.get('businessCode', ''),
+ detail.get('linkedCodes') or {},
+ )
+ run_db_write(_write_refresh)
with open_db_connection() as conn:
ensure_db_schema(conn)
cached = get_erp_contract_detail_cache(conn, page_name, project_code)
+ if cached and (
+ (
+ not (cached.get('linkedCodes') or {}).get('salesCode')
+ and not (cached.get('linkedCodes') or {}).get('designCode')
+ ) or (
+ (cached.get('linkedCodes') or {}).get('salesCode')
+ and not (cached.get('linkedCodes') or {}).get('salesName')
+ ) or (
+ (cached.get('linkedCodes') or {}).get('designCode')
+ and not (cached.get('linkedCodes') or {}).get('designName')
+ )
+ ):
+ linked_codes = find_erp_linked_codes(project_code, cached.get('businessCode', ''), cached.get('projectName', '') or project_name)
+ if linked_codes.get('salesCode') or linked_codes.get('designCode'):
+ cached['linkedCodes'] = linked_codes
+ def _write_backfill(conn):
+ replace_erp_contract_detail_cache(conn, cached)
+ replace_erp_linked_code_cache(
+ conn,
+ page_name,
+ project_code,
+ cached.get('projectName', '') or project_name,
+ cached.get('businessCode', ''),
+ linked_codes,
+ )
+ run_db_write(_write_backfill)
+ with open_db_connection() as conn:
+ ensure_db_schema(conn)
+ cached = get_erp_contract_detail_cache(conn, page_name, project_code)
if not cached:
return self._json(404, {'ok': False, 'error': '계약정보 캐시가 없습니다.'})
return self._json(
@@ -4480,9 +4875,16 @@ class Handler(BaseHTTPRequestHandler):
if parsed.path == '/api/sync-project-aliases':
try:
with sqlite3.connect(DB_PATH) as conn:
- alias_rows = load_project_alias_from_erp()
- replace_project_alias(conn, alias_rows)
- return self._json(200, {'ok': True, 'project_alias_source': 'erp', 'project_alias_loaded': len(alias_rows)})
+ ensure_db_schema(conn)
+ sync_info = sync_project_alias_caches(conn)
+ linked_count = rebuild_erp_linked_code_cache(conn, 'const')
+ return self._json(200, {
+ 'ok': True,
+ 'project_alias_source': 'erp',
+ 'project_alias_loaded': len(sync_info['entries']),
+ 'linked_code_cache_loaded': linked_count,
+ 'syncedAt': sync_info['cacheInfo']['syncedAt'],
+ })
except Exception as e:
traceback.print_exc()
return self._json(500, {'ok': False, 'error': str(e)})
@@ -4503,7 +4905,11 @@ class Handler(BaseHTTPRequestHandler):
q = parse_qs(parsed.query)
page_name = q.get('page', ['const'])[0]
result = fetch_erp_project_codes(page_name)
- sync_info = run_db_write(lambda conn: replace_erp_project_code_cache(conn, result['page'], result['rows']))
+ def _write_project_codes(conn):
+ sync_info = replace_erp_project_code_cache(conn, result['page'], result['rows'])
+ linked_count = rebuild_erp_linked_code_cache(conn, result['page']) if result['page'] == 'const' else 0
+ return sync_info, linked_count
+ sync_info, linked_count = run_db_write(_write_project_codes)
return self._json(
200,
{
@@ -4511,6 +4917,7 @@ class Handler(BaseHTTPRequestHandler):
'count': sync_info['count'],
'page': sync_info['sourcePage'],
'syncedAt': sync_info['syncedAt'],
+ 'linkedCodeCacheLoaded': linked_count,
},
)
except Exception as e:
@@ -4524,7 +4931,18 @@ 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))
+ def _write_contract(conn):
+ sync_info = replace_erp_contract_detail_cache(conn, detail)
+ replace_erp_linked_code_cache(
+ conn,
+ page_name,
+ project_code,
+ detail.get('projectName', '') or project_name,
+ detail.get('businessCode', ''),
+ detail.get('linkedCodes') or {},
+ )
+ return sync_info
+ sync_info = run_db_write(_write_contract)
with open_db_connection() as conn:
ensure_db_schema(conn)
cached = get_erp_contract_detail_cache(conn, page_name, project_code)
@@ -4588,13 +5006,14 @@ class Handler(BaseHTTPRequestHandler):
ensure_db_schema(conn)
used = load_from_mysql_into_sqlite(conn)
try:
- alias_rows = load_project_alias_from_erp()
+ sync_info = sync_project_alias_caches(conn)
used['project_alias_source'] = 'erp'
- replace_project_alias(conn, alias_rows)
- used['project_alias_loaded'] = len(alias_rows)
+ used['project_alias_loaded'] = len(sync_info['entries'])
+ used['linked_code_cache_loaded'] = rebuild_erp_linked_code_cache(conn, 'const')
except Exception as e:
used['project_alias_source'] = 'erp_failed_keep_existing'
used['project_alias_loaded'] = 0
+ used['linked_code_cache_loaded'] = 0
used['project_alias_error'] = str(e)
# member rank는 MySQL member.rankCode -> systemconfig 매핑을 우선 사용
# (used['member_rank_source'], used['member_rank_loaded']는 load_from_mysql_into_sqlite에서 설정)
@@ -4633,9 +5052,10 @@ if __name__ == '__main__':
init_db(conn)
if os.environ.get('STARTUP_MAINTENANCE', '1') not in ('0', 'false', 'False', 'no'):
try:
- alias_rows = load_project_alias_from_erp()
- print(f'Loaded project aliases from ERP: {len(alias_rows)}')
- replace_project_alias(conn, alias_rows)
+ sync_info = sync_project_alias_caches(conn)
+ print(f"Loaded project aliases from ERP: {len(sync_info['entries'])}")
+ linked_count = rebuild_erp_linked_code_cache(conn, 'const')
+ print(f'Loaded linked code cache: {linked_count}')
except Exception as e:
print(f'Load project aliases from ERP failed, keep existing aliases: {e}')
# 직급은 MySQL member.rankCode -> systemconfig 매핑 기반으로 /api/load 시 반영
diff --git a/project-codes.html b/project-codes.html
index a7dafbc..bda559d 100644
--- a/project-codes.html
+++ b/project-codes.html
@@ -635,6 +635,7 @@