diff --git a/backend-node/src/controllers/screenGroupController.ts b/backend-node/src/controllers/screenGroupController.ts
index a942861c..c5e15263 100644
--- a/backend-node/src/controllers/screenGroupController.ts
+++ b/backend-node/src/controllers/screenGroupController.ts
@@ -1184,6 +1184,12 @@ export const getScreenSubTables = async (req: Request, res: Response) => {
relationType: string; // 'join' | 'lookup' | 'source' | 'reference'
fieldMappings?: Array<{ sourceField: string; targetField: string; sourceDisplayName?: string; targetDisplayName?: string }>;
}>;
+ saveTables?: Array<{
+ tableName: string;
+ saveType: 'save' | 'edit' | 'delete' | 'transferData';
+ componentType: string;
+ isMainTable: boolean;
+ }>;
}> = {};
// 1. 기존 방식: componentConfig에서 tableName, sourceTable, fieldMappings 추출
@@ -1892,13 +1898,74 @@ export const getScreenSubTables = async (req: Request, res: Response) => {
});
});
+ // ============================================================
+ // 저장 테이블 정보 추출
+ // ============================================================
+ const saveTableQuery = `
+ SELECT DISTINCT
+ sd.screen_id,
+ sd.screen_name,
+ sd.table_name as main_table,
+ sl.properties->'componentConfig'->'action'->>'type' as action_type,
+ sl.properties->>'componentType' as component_type,
+ sl.properties->'componentConfig'->>'targetTable' as target_table,
+ sl.properties->'componentConfig'->'action'->'dataTransfer'->>'targetTable' as transfer_target_table
+ FROM screen_definitions sd
+ JOIN screen_layouts sl ON sd.screen_id = sl.screen_id
+ WHERE sd.screen_id = ANY($1)
+ AND sl.properties->'componentConfig'->'action'->>'type' = 'save'
+ AND sl.properties->'componentConfig'->'action'->>'targetScreenId' IS NULL
+ ORDER BY sd.screen_id
+ `;
+
+ const saveTableResult = await pool.query(saveTableQuery, [screenIds]);
+
+ saveTableResult.rows.forEach((row: any) => {
+ const screenId = row.screen_id;
+ const mainTable = row.main_table;
+ const actionType = row.action_type as 'save' | 'edit' | 'delete' | 'transferData';
+ const componentType = row.component_type || 'component';
+ const targetTable = row.target_table || row.transfer_target_table || mainTable;
+
+ // 화면 정보가 없으면 초기화
+ if (!screenSubTables[screenId]) {
+ screenSubTables[screenId] = {
+ screenId,
+ screenName: row.screen_name,
+ mainTable: mainTable || '',
+ subTables: [],
+ saveTables: [],
+ };
+ }
+
+ // saveTables 배열 초기화
+ if (!screenSubTables[screenId].saveTables) {
+ screenSubTables[screenId].saveTables = [];
+ }
+
+ // 중복 체크
+ const existingSaveTable = screenSubTables[screenId].saveTables!.find(
+ (st) => st.tableName === targetTable && st.saveType === actionType
+ );
+
+ if (!existingSaveTable && targetTable) {
+ screenSubTables[screenId].saveTables!.push({
+ tableName: targetTable,
+ saveType: actionType,
+ componentType,
+ isMainTable: targetTable === mainTable,
+ });
+ }
+ });
+
logger.info("화면 서브 테이블 정보 조회 완료", {
screenIds,
resultCount: Object.keys(screenSubTables).length,
details: Object.values(screenSubTables).map(s => ({
screenId: s.screenId,
mainTable: s.mainTable,
- subTables: s.subTables.map(st => st.tableName)
+ subTables: s.subTables.map(st => st.tableName),
+ saveTables: s.saveTables?.map(st => st.tableName) || []
}))
});
diff --git a/docs/화면관계_시각화_개선_보고서.md b/docs/화면관계_시각화_개선_보고서.md
index 20edf254..fcd08fae 100644
--- a/docs/화면관계_시각화_개선_보고서.md
+++ b/docs/화면관계_시각화_개선_보고서.md
@@ -1053,19 +1053,39 @@ screenSubTables[screenId].subTables.push({
10. **방안 C 적용: 필터선 제거 + 보라색 테두리 애니메이션**
- 필터 관계는 선 없이 뱃지 + 테이블 테두리로만 표시 (겹침 방지)
- - 필터링된 테이블에 **보라색 테두리 + 펄스 애니메이션** 적용
+ - 필터링된 테이블에 **보라색 테두리** 적용 (부드러운 색상 전환)
- 조인선(주황)만 표시, 필터선(보라) 제거
+11. **테이블 높이 부드러운 애니메이션**
+ - 포커스 시 컬럼 목록이 변경될 때 **부드러운 높이 전환** 적용
+ - `transition: height 0.5s cubic-bezier(0.4, 0, 0.2, 1)` 사용
+ - **Debounce 로직** (50ms): 듀얼 그리드에서 filterColumns와 joinColumns가 2단계로 업데이트되는 문제 해결
+ - 중간 값(늘어났다가 줄어드는 현상) 무시, 최종 값만 적용
+
+12. **뱃지 영역 레이아웃 개선**
+ - 뱃지를 컬럼 목록 영역 **안에 포함** (높이 늘어남 방지)
+ - `calculatedHeight`에 뱃지 높이(26px) 포함하여 계산
+ - 뱃지와 컬럼 동시 변경으로 "늘어났다가 줄어드는" 현상 해결
+
+13. **뱃지 스타일 개선**
+ - 회색 테두리 (`border-slate-300`) + 연한 배경 (`bg-slate-50`)
+ - 보라색 컬럼과 확실히 구분되는 디자인
+ - 필터 태그: 보라색 pill 스타일 (`rounded-full bg-violet-600`)
+
**시각적 표현:**
| 관계 유형 | 선 표시 | 테두리 | 배지 |
|----------|---------|--------|------|
| 조인 | ✅ 주황색 점선 | - | "조인" |
-| 필터 | ❌ 없음 | 보라색 펄스 | "필터 + 키값" |
+| 필터 | ❌ 없음 | 보라색 (부드러운 전환) | "필터 + 키값" |
| 룩업 | ✅ 황색 점선 | - | "N곳 참조" |
**구현 상세:**
- `ScreenRelationFlow.tsx`: `visualRelationType === 'filter'`인 경우 엣지 생성 건너뛰기
-- `ScreenNode.tsx`: `hasFilterRelation` 조건으로 보라색 테두리 + `animate-pulse` 클래스 적용
+- `ScreenNode.tsx`:
+ - `hasFilterRelation` 조건으로 보라색 테두리 + 부드러운 색상 전환 적용
+ - `calculatedHeight`에 뱃지 높이 포함
+ - `debouncedHeight` 사용으로 중간 값 무시
+ - 뱃지를 컬럼 목록 div 안에 배치
### 향후 개선 가능 사항
@@ -1087,9 +1107,182 @@ screenSubTables[screenId].subTables.push({
8. [x] FK 컬럼 보라색 강조 + 키값 정보 표시
9. [x] 포커스 상태 기반 필터 표시 개선
10. [x] 필터 컬럼 상단 정렬 (조인 → 필터 → 사용 순서)
-11. [x] 방안 C 적용: 필터선 제거 + 보라색 테두리 애니메이션
-12. [ ] 범례 UI 추가 (선택사항)
-13. [ ] 엣지 라벨에 관계 유형 표시 (선택사항)
+11. [x] 방안 C 적용: 필터선 제거 + 보라색 테두리 (펄스 → 부드러운 전환으로 변경)
+12. [x] 테이블 높이 부드러운 애니메이션 + Debounce 적용
+13. [x] 뱃지 영역 레이아웃 개선 (컬럼 목록 안에 포함)
+14. [x] 뱃지 스타일 개선 (회색 테두리로 컬럼과 구분)
+15. [x] 서브테이블 Y 좌표 조정 (690px → 740px)
+16. [x] **저장 테이블 시각화** (구현 완료)
+17. [x] 테이블 스크롤 기능 추가 (maxHeight + overflow-y-auto)
+18. [x] 테이블/헤더 둥근 모서리 (rounded-xl, rounded-t-xl)
+19. [x] 필터 테이블 조인선 + 참조 테이블 활성화
+20. [x] 조인선 색상 상수 통일 (RELATION_COLORS.join.stroke)
+21. [ ] **선 교차점 이질감 해결** (계획 중)
+22. [ ] 범례 UI 추가 (선택사항)
+23. [ ] 엣지 라벨에 관계 유형 표시 (선택사항)
+
+---
+
+## 저장 테이블 시각화 (구현 완료)
+
+### 개요
+화면에서 데이터가 **어떤 테이블에 저장**되는지 시각화
+
+### 저장 테이블 유형
+
+| 유형 | 설명 | 예시 |
+|------|------|------|
+| **메인 저장** | 화면의 메인 테이블에 직접 저장 | 수주등록 → `sales_order_mng` |
+| **연계 저장** | 버튼 클릭 → 다른 화면의 테이블에 저장 | 수주관리 → 출하계획 → `shipment_plan` |
+| **서브 저장** | 듀얼 그리드에서 서브 테이블에 저장 | 거래처관리 → `customer_item_mapping` |
+
+### 데이터 수집 방법 (백엔드)
+
+저장 테이블 정보를 찾을 수 있는 곳:
+1. `componentConfig.action.type = 'save'` (edit, delete 제외)
+2. `componentConfig.targetTable` (modal-repeater-table 등)
+3. `action.dataTransfer.targetTable` (데이터 전송 대상)
+4. **제외 조건**: `action.targetScreenId IS NOT NULL` (모달 열기 버튼)
+
+### 시각적 표현 (구현됨)
+
+**핑크색 막대기 표시**
+- 테이블 노드 **왼쪽 바깥**에 핑크색 세로 막대기 표시
+- 위에서 아래로 나타나는 애니메이션 (`scaleY` 트랜지션)
+- 포커스 해제 시 사라지는 애니메이션
+- 막대기 양끝 그라데이션 (투명 → 핑크 → 투명)
+
+**스타일:**
+```css
+/* 저장 막대기 스타일 */
+position: absolute;
+left: -6px; /* -left-1.5 */
+top: 4px;
+bottom: 4px;
+width: 2px; /* w-0.5 */
+background: linear-gradient(
+ to bottom,
+ transparent 0%,
+ #f472b6 15%, /* pink-400 */
+ #f472b6 85%,
+ transparent 100%
+);
+transition: all 0.5s ease-out;
+transform-origin: top;
+```
+
+**애니메이션:**
+- 포커스 시: `opacity: 1, scaleY: 1` (나타남)
+- 포커스 해제 시: `opacity: 0, scaleY: 0` (사라짐)
+
+### 색상 팔레트
+
+| 관계 유형 | 선 색상 | 뱃지/막대 색상 | 컬럼 강조 |
+|----------|---------|---------------|----------|
+| 조인 | 주황 (#F97316) | 주황 | 주황 |
+| 필터 | - | 보라 (#8B5CF6) | 보라 |
+| 룩업 | 황색 (#EAB308) | 황색 | - |
+| **저장** | - | 핑크 (#F472B6) | - |
+
+### 구현 단계 (완료)
+
+1. [x] 백엔드: `getScreenSubTables`에서 저장 테이블 정보 추출
+2. [x] 타입 정의: `SaveTableInfo` 인터페이스 추가
+3. [x] 프론트엔드: 핑크색 막대기 UI 구현
+4. [x] 프론트엔드: 포커싱 시에만 표시
+5. [x] 프론트엔드: 나타나기/사라지기 애니메이션
+6. [ ] 프론트엔드: 뱃지 클릭 시 팝오버 상세정보 (향후)
+
+---
+
+## 필터 테이블 조인선 시각화 (구현 완료)
+
+### 개요
+마스터-디테일 관계에서 **필터 대상 테이블**이 **다른 테이블과 조인**하는 경우도 시각화
+
+### 시나리오
+"거래처관리 화면" (1번 화면) 포커싱 시:
+- `customer_mng` (마스터) → `customer_item_mapping` (디테일) 필터 관계
+- `customer_item_mapping` → `item_info` **조인 관계** (품목 ID → 품번)
+
+### 구현 내용
+
+1. **화면 → 필터 대상 테이블 연결선**
+ - 파란색 점선으로 화면 → `customer_item_mapping` 연결
+ - 기존 `customer_mng`로만 가던 연결 외에 추가
+
+2. **필터 대상 테이블의 조인선**
+ - `customer_item_mapping` → `item_info` 주황색 점선 조인선
+ - `joinColumnRefs` 기반으로 자동 생성
+
+3. **참조 테이블 활성화**
+ - `item_info` 테이블도 함께 활성화 (회색 처리 안 함)
+ - 조인 컬럼 주황색 강조 표시
+
+### 포커싱 제어
+- 해당 화면이 포커싱됐을 때만 조인선 활성화
+- 다른 화면 포커싱 시 흐리게 처리 (opacity: 0.3)
+
+### 코드 위치
+- `ScreenRelationFlow.tsx`: 필터 조인 엣지 생성 로직
+- `styledNodes`: 필터 대상 테이블의 조인 참조 테이블 활성화 로직
+
+---
+
+## 테이블 노드 UI 개선 (구현 완료)
+
+### 스크롤 기능
+- 컬럼이 많을 경우 스크롤 가능 (`overflow-y-auto`)
+- 최대 높이 제한 (`maxHeight: 300px`)
+- 얇은 스크롤바 (`scrollbar-thin`)
+
+### 둥근 모서리
+- 테이블 전체: `rounded-xl` (12px)
+- 헤더: `rounded-t-xl` (상단만 12px)
+
+### 조인선 색상 통일
+- 모든 조인선이 `RELATION_COLORS.join.stroke` 상수 사용
+- 기본 색상: `#f97316` (orange-500)
+- 강조 색상: `#ea580c` (orange-600)
+
+---
+
+## [계획] 선 교차점 이질감 해결
+
+> **상태**: 방안 검토 중 (미구현)
+
+### 배경
+여러 파란색 연결선이 서로 교차할 때 시각적 이질감 발생
+
+### 해결 방안
+
+#### 방안 C: 배경색 테두리 (Outline) - 권장
+- 각 선에 **흰색 테두리(outline)** 추가
+- 교차할 때 위에 있는 선이 아래 선을 "덮는" 효과
+- SVG stroke에 흰색 outline 적용
+
+**구현 방식:**
+```typescript
+// 커스텀 엣지 컴포넌트에서
+