fix(wbs-ux): structure FactorParamDetailLayout as a distinct 6:4 Split Pane with physical Split Divider Bar
Validators (Pushes and Pull Requests) / UI & Storage Validation (push) Failing after 10s
Validators (Pushes and Pull Requests) / Database & Schema Validation (push) Successful in 10s
Validators (Pushes and Pull Requests) / Core Validators & Database Setup (push) Failing after 19s
Validators (Pushes and Pull Requests) / WBS & Audit Validations (push) Has been skipped
Validators (Pushes and Pull Requests) / .NET Contracts (push) Has been skipped
Validators (Pushes and Pull Requests) / Calibration & Performance (push) Has been skipped
Validators (Pushes and Pull Requests) / Operational Report & Decision Packet (push) Has been skipped
Validators (Pushes and Pull Requests) / Security & Secrets (push) Successful in 10s
Validators (Pushes and Pull Requests) / CI Workflow Lint (push) Failing after 10s
Validators (Pushes and Pull Requests) / Notify PR Results (push) Has been skipped

This commit is contained in:
2026-07-25 20:20:59 +09:00
parent 996356e551
commit 7530e45587
@@ -73,136 +73,214 @@ onMounted(loadFactors);
</script>
<template>
<div class="split-layout-container">
<!-- 좌측 마스터 테이블 영역 -->
<div class="master-panel">
<div class="panel-header">
<h3 class="panel-title">📊 팩터 수식 원장 (factor_version_history)</h3>
<span class="panel-subtitle">수식을 클릭하면 우측 상세 매개변수가 바인딩됩니다.</span>
</div>
<div class="panel-body">
<table class="douzone-grid-table">
<thead>
<tr>
<th>팩터 ID</th>
<th>수식 명칭</th>
<th>카테고리</th>
<th>보정 상태</th>
</tr>
</thead>
<tbody>
<tr v-for="item in factors" :key="item.factor_id"
@click="selectFactor(item)"
:class="{ 'selected-row': selectedFactor?.factor_id === item.factor_id }">
<td class="font-mono font-bold">{{ item.factor_id }}</td>
<td>{{ item.formula_name }}</td>
<td><span class="badge badge-category">{{ item.category }}</span></td>
<td>
<span class="badge badge-status" :class="item.calibration_state.toLowerCase()">
{{ item.calibration_state }}
</span>
</td>
</tr>
</tbody>
</table>
</div>
<div class="split-screen-wrapper">
<div class="page-top-bar">
<h3 class="page-title">타입 A: 마스터-디테일 스플릿 화면 (Master-Detail Split View)</h3>
<span class="page-desc">좌측 수식 목록과 우측 파라미터 제어 폼이 6:4 비율 Split Pane으로 세분화됩니다.</span>
</div>
<!-- 우측 디테일 영역 -->
<div class="detail-panel" v-if="selectedFactor">
<div class="panel-header">
<h3 class="panel-title"> 임계 한도 매개변수 설정 [{{ selectedFactor.factor_id }}]</h3>
<!-- 2 Split 구도 컨테이너 -->
<div class="split-pane-container">
<!-- 1. 좌측 마스터 패널 (60%) -->
<div class="master-panel">
<div class="panel-header">
<span class="panel-badge">Master List</span>
<h4 class="panel-heading">📊 팩터 수식 원장 (factor_version_history)</h4>
</div>
<div class="panel-content">
<table class="douzone-grid-table">
<thead>
<tr>
<th>팩터 ID</th>
<th>수식 명칭</th>
<th>카테고리</th>
<th>보정 상태</th>
</tr>
</thead>
<tbody>
<tr v-for="item in factors" :key="item.factor_id"
@click="selectFactor(item)"
:class="{ 'selected-row': selectedFactor?.factor_id === item.factor_id }">
<td class="font-mono font-bold">{{ item.factor_id }}</td>
<td>{{ item.formula_name }}</td>
<td><span class="badge badge-category">{{ item.category }}</span></td>
<td>
<span class="badge badge-status" :class="item.calibration_state.toLowerCase()">
{{ item.calibration_state }}
</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="panel-body form-body">
<div class="form-group">
<label class="form-label">팩터 명칭</label>
<input type="text" class="form-input readonly" :value="selectedFactor.formula_name" readonly />
</div>
<div class="form-group">
<label class="form-label">상한 Threshold (Upper Limit)</label>
<input type="number" class="form-input" v-model.number="selectedFactor.threshold_params.upper" />
</div>
<div class="form-group">
<label class="form-label">하한 Threshold (Lower Limit)</label>
<input type="number" class="form-input" v-model.number="selectedFactor.threshold_params.lower" />
</div>
<div class="form-group">
<label class="form-label">보정 상태 (Calibration State)</label>
<select class="form-input" v-model="selectedFactor.calibration_state">
<option value="CALIBRATED">CALIBRATED (검증 완료)</option>
<option value="OPTIMIZING">OPTIMIZING (최적화 )</option>
<option value="STALE">STALE (만료됨)</option>
</select>
</div>
<button class="btn-save-action" :disabled="isSaving" @click="saveThreshold">
{{ isSaving ? '저장 처리 중...' : '💾 PostgreSQL 원장 업데이트 저장' }}
</button>
<!-- 2. 중앙 물리 스플릿 구분선 (Split Divider Bar) -->
<div class="split-divider">
<div class="divider-line"></div>
</div>
<!-- 3. 우측 디테일 패널 (40%) -->
<div class="detail-panel" v-if="selectedFactor">
<div class="panel-header">
<span class="panel-badge badge-active">Detail Form</span>
<h4 class="panel-heading"> 임계 한도 매개변수 설정 [{{ selectedFactor.factor_id }}]</h4>
</div>
<div class="panel-content form-container">
<div class="form-group">
<label class="form-label">팩터 식별자</label>
<input type="text" class="form-input readonly" :value="selectedFactor.factor_id" readonly />
</div>
<div class="form-group">
<label class="form-label">수식 명칭</label>
<input type="text" class="form-input readonly" :value="selectedFactor.formula_name" readonly />
</div>
<div class="form-group">
<label class="form-label">상한 Threshold (Upper Limit)</label>
<input type="number" class="form-input" v-model.number="selectedFactor.threshold_params.upper" />
</div>
<div class="form-group">
<label class="form-label">하한 Threshold (Lower Limit)</label>
<input type="number" class="form-input" v-model.number="selectedFactor.threshold_params.lower" />
</div>
<div class="form-group">
<label class="form-label">보정 상태 (Calibration State)</label>
<select class="form-input" v-model="selectedFactor.calibration_state">
<option value="CALIBRATED">CALIBRATED (검증 완료)</option>
<option value="OPTIMIZING">OPTIMIZING (최적화 )</option>
<option value="STALE">STALE (만료됨)</option>
</select>
</div>
<button class="btn-save-action" :disabled="isSaving" @click="saveThreshold">
{{ isSaving ? '저장 처리 중...' : '💾 PostgreSQL 원장 업데이트 저장' }}
</button>
</div>
</div>
<!-- 선택 없을 스플릿 디테일 엠티 패널 -->
<div class="detail-panel empty-panel" v-else>
<div class="panel-header">
<span class="panel-badge">Detail Form</span>
<h4 class="panel-heading"> 임계 한도 매개변수 설정</h4>
</div>
<div class="empty-message-box">
<span class="empty-icon">👈</span>
<p class="empty-title">팩터 항목이 선택되지 않았습니다.</p>
<p class="empty-desc">좌측 마스터 테이블에서 수식 항목을 클릭하면 40% 영역에 상세 설정 폼이 바인딩됩니다.</p>
</div>
</div>
</div>
<div class="detail-panel empty-panel" v-else>
<p>좌측 팩터 수식을 선택하면 상세 파라미터 편집 폼이 노출됩니다.</p>
</div>
</div>
</template>
<style scoped>
.split-layout-container {
.split-screen-wrapper {
display: flex;
flex-direction: column;
height: calc(100vh - 110px);
background-color: #F1F5F9;
gap: 16px;
padding: 16px;
background-color: #F1F5F9;
box-sizing: border-box;
gap: 12px;
}
.master-panel {
flex: 2;
background: #FFFFFF;
border: 1px solid #CBD5E1;
border-radius: 6px;
display: flex;
flex-direction: column;
overflow: hidden;
.page-top-bar {
border-bottom: 1px solid #CBD5E1;
padding-bottom: 8px;
}
.detail-panel {
flex: 1;
background: #FFFFFF;
border: 1px solid #CBD5E1;
border-radius: 6px;
display: flex;
flex-direction: column;
overflow: hidden;
}
.empty-panel {
justify-content: center;
align-items: center;
color: #94A3B8;
font-size: 0.85rem;
padding: 24px;
text-align: center;
}
.panel-header {
padding: 12px 16px;
background-color: #F8FAFC;
border-bottom: 1px solid #E2E8F0;
}
.panel-title {
font-size: 0.9rem;
.page-title {
font-size: 1rem;
font-weight: 700;
color: #1E293B;
margin: 0;
}
.panel-subtitle {
.page-desc {
font-size: 0.75rem;
color: #64748B;
}
.panel-body {
.split-pane-container {
display: flex;
flex: 1;
width: 100%;
overflow: hidden;
}
.master-panel {
flex: 6;
background: #FFFFFF;
border: 1px solid #CBD5E1;
border-radius: 6px 0 0 6px;
display: flex;
flex-direction: column;
overflow: hidden;
}
.split-divider {
width: 8px;
background-color: #CBD5E1;
display: flex;
justify-content: center;
align-items: center;
cursor: col-resize;
transition: background-color 0.2s ease;
}
.split-divider:hover {
background-color: #2563EB;
}
.divider-line {
width: 2px;
height: 30px;
background-color: #94A3B8;
border-radius: 1px;
}
.detail-panel {
flex: 4;
background: #FFFFFF;
border: 1px solid #CBD5E1;
border-left: none;
border-radius: 0 6px 6px 0;
display: flex;
flex-direction: column;
overflow: hidden;
}
.panel-header {
padding: 10px 16px;
background-color: #F8FAFC;
border-bottom: 1px solid #E2E8F0;
display: flex;
align-items: center;
gap: 8px;
}
.panel-badge {
font-size: 0.65rem;
font-weight: 700;
padding: 2px 6px;
background-color: #E2E8F0;
color: #475569;
border-radius: 3px;
}
.panel-badge.badge-active {
background-color: #2563EB;
color: #FFFFFF;
}
.panel-heading {
font-size: 0.85rem;
font-weight: 700;
color: #1E293B;
margin: 0;
}
.panel-content {
flex: 1;
overflow-y: auto;
padding: 12px;
@@ -242,13 +320,8 @@ onMounted(loadFactors);
font-weight: 700;
}
.font-mono {
font-family: monospace;
}
.font-bold {
font-weight: 700;
}
.font-mono { font-family: monospace; }
.font-bold { font-weight: 700; }
.badge {
display: inline-block;
@@ -258,31 +331,21 @@ onMounted(loadFactors);
font-weight: 700;
}
.badge-category {
background-color: #E2E8F0;
color: #475569;
}
.badge-category { background-color: #E2E8F0; color: #475569; }
.badge-status.calibrated { background-color: #DCFCE7; color: #166534; }
.badge-status.optimizing { background-color: #FEF3C7; color: #92400E; }
.badge-status.calibrated {
background-color: #DCFCE7;
color: #166534;
}
.badge-status.optimizing {
background-color: #FEF3C7;
color: #92400E;
}
.form-body {
.form-container {
display: flex;
flex-direction: column;
gap: 16px;
gap: 14px;
padding: 16px;
}
.form-group {
display: flex;
flex-direction: column;
gap: 6px;
gap: 4px;
}
.form-label {
@@ -297,11 +360,6 @@ onMounted(loadFactors);
border-radius: 4px;
font-size: 0.85rem;
outline: none;
transition: border-color 0.15s ease;
}
.form-input:focus {
border-color: #2563EB;
}
.form-input.readonly {
@@ -319,15 +377,22 @@ onMounted(loadFactors);
border: none;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.15s ease;
}
.btn-save-action:hover {
background-color: #1D4ED8;
.btn-save-action:hover { background-color: #1D4ED8; }
.empty-message-box {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 24px;
text-align: center;
color: #64748B;
}
.btn-save-action:disabled {
background-color: #94A3B8;
cursor: not-allowed;
}
.empty-icon { font-size: 2rem; margin-bottom: 8px; }
.empty-title { font-size: 0.9rem; font-weight: 700; color: #334155; margin: 0 0 4px 0; }
.empty-desc { font-size: 0.75rem; color: #94A3B8; margin: 0; max-width: 240px; }
</style>