552 lines
23 KiB
Markdown
552 lines
23 KiB
Markdown
# POP 개발 계획
|
|
|
|
---
|
|
|
|
## 현재 상태 (2026-02-11)
|
|
|
|
**Phase 0 공통 인프라 (usePopEvent + useDataSource) 구현 완료, ksh-v2-work 병합 + 원격 push 완료. Phase 2 pop-button 설계 진행 중.**
|
|
|
|
---
|
|
|
|
## 작업 순서
|
|
|
|
```
|
|
[Phase 1~3] [Phase 5] [정의서] [Phase 0~6]
|
|
v4 Flexbox → v5 CSS Grid → 컴포넌트 설계 → 실제 구현
|
|
완료 완료 (v5.2) 완료 (v8.0) 다음
|
|
```
|
|
|
|
---
|
|
|
|
## 완료된 Phase
|
|
|
|
### Phase 1~3: v4 Flexbox 시스템 (완료, 레거시 삭제됨)
|
|
|
|
v4 Flexbox 기반 시스템은 v5 CSS Grid로 완전히 대체되었습니다.
|
|
v4 관련 파일은 모두 삭제되었습니다.
|
|
|
|
- [x] v4 기본 구조, 렌더러, 디자이너 통합
|
|
- [x] Undo/Redo, 드래그 리사이즈, Flexbox 가로 배치
|
|
- [x] 비율 스케일링 시스템
|
|
- [x] 오버라이드 기능 (모드별 배치 고정)
|
|
- [x] 컴포넌트 표시/숨김, 줄바꿈
|
|
|
|
### Phase 5: v5 CSS Grid 시스템 (완료)
|
|
|
|
#### Phase 5.1: 타입 정의 (완료)
|
|
- [x] `PopLayoutDataV5` 인터페이스
|
|
- [x] `PopGridConfig`, `PopGridPosition` 타입
|
|
- [x] `GridMode`, `GRID_BREAKPOINTS` 상수
|
|
- [x] `createEmptyPopLayoutV5()`, `isV5Layout()`, `detectGridMode()`
|
|
|
|
#### Phase 5.2: 그리드 렌더러 (완료)
|
|
- [x] `PopRenderer.tsx` - CSS Grid 기반 렌더링
|
|
- [x] 격자 셀 렌더링 (CSS Grid 동일 좌표계)
|
|
- [x] 위치 변환 (12칸 -> 4/6/8칸)
|
|
|
|
#### Phase 5.3: 디자이너 UI (완료)
|
|
- [x] `PopCanvas.tsx` - 그리드 캔버스 + 행/열 라벨
|
|
- [x] 드래그 스냅 (칸에 맞춤)
|
|
- [x] `ComponentEditorPanel.tsx` - 위치 편집
|
|
|
|
#### Phase 5.4: 반응형 자동화 (완료)
|
|
- [x] 자동 변환 알고리즘 (12칸 -> 4칸)
|
|
- [x] 겹침 감지 및 재배치
|
|
- [x] 모드별 오버라이드 저장
|
|
|
|
#### v5.1 추가 기능 (완료)
|
|
- [x] 자동 줄바꿈 (col > maxCol -> 맨 아래 배치)
|
|
- [x] "검토 필요" 알림 시스템
|
|
- [x] Gap 프리셋 (좁게/보통/넓게)
|
|
- [x] 숨김 기능 (모드별)
|
|
|
|
#### v5.2 브레이크포인트 재설계 + 세로 자동 확장 (완료)
|
|
- [x] 기기 기반 브레이크포인트 (479/767/1023px)
|
|
- [x] 세로 자동 확장 (dynamicCanvasHeight)
|
|
- [x] 뷰어 반응형 일관성 (detectGridMode 사용)
|
|
- [x] VIEWPORT_PRESETS에서 height 제거
|
|
|
|
### 컴포넌트 설계 (완료)
|
|
|
|
- [x] 9개 컴포넌트 정의 (POPUPDATE_2.md v8.0)
|
|
- [x] POP 헌법 9조 작성
|
|
- [x] 공통 인프라 설계 (DataSourceConfig, ColumnBinding, JoinConfig, useDataSource, usePopEvent, PopActionConfig)
|
|
- [x] 모달 화면 설계 방식 확정 (인라인 + 외부 참조)
|
|
- [x] 기존 시스템 호환성 검증 (DB/백엔드/프론트 변경 불필요 확인)
|
|
|
|
---
|
|
|
|
## 다음 작업
|
|
|
|
### Phase 0: 공통 인프라
|
|
|
|
모든 데이터 연동 컴포넌트가 공유하는 기반 시스템:
|
|
|
|
- [x] ColumnBinding 타입 정의 (read/write/readwrite/hidden) -- types.ts에 추가 완료
|
|
- [x] JoinConfig 타입 정의 (테이블 조인) -- types.ts에 추가 완료
|
|
- [x] DataSourceConfig 타입 정의 (데이터 소스 설정) -- types.ts에 추가 완료
|
|
- [x] PopActionConfig 타입 정의 (액션 설정) -- types.ts에 추가 완료
|
|
- [x] usePopEvent 훅 구현 (이벤트 버스, 데이터 전달, 화면 단위 격리) -- 완료
|
|
- [x] useDataSource 훅 구현 (CRUD 포함, 기존 dataApi 활용) -- 완료
|
|
|
|
### Phase 1: pop-dashboard -- 완료
|
|
|
|
> **2026-02-10**: 17단계 코딩 + 검수 + 팔레트 등록 완료
|
|
|
|
- [x] PopDashboardConfig, DashboardItem 타입 정의
|
|
- [x] 멀티 아이템 컨테이너 구현 (여러 아이템 묶음)
|
|
- [x] 4개 서브타입: kpi-card, chart, gauge, stat-card
|
|
- [x] 4개 표시 모드: arrows, auto-slide, grid, scroll
|
|
- [x] 계산식(formula) 지원: "생산량/총재고량" 같은 복합 표현
|
|
- [x] 설정 패널: 드롭다운 기반 쉬운 집계 설정 (SQL 불필요)
|
|
- [x] PopComponentRegistry 등록 + 디자이너 팔레트 등록
|
|
- [ ] 이벤트: filter_changed 수신, kpi_clicked 발행 -- usePopEvent 완성 후
|
|
- [ ] 기존 `components/pop/dashboard/` 폴더 폐기 (모든 기능 대체 확인 후)
|
|
|
|
### Phase 2: pop-button, pop-icon
|
|
|
|
- [ ] pop-button: 저장/삭제/API 호출 액션
|
|
- [ ] pop-icon: 화면 이동/URL/새로고침
|
|
|
|
### Phase 3: pop-table (테이블형 우선)
|
|
|
|
- [ ] table-list 서브타입: 행/열 장부형
|
|
- [ ] ColumnBinding 기반 컬럼별 read/write
|
|
- [ ] card-list 서브타입: 카드 템플릿 (후순위)
|
|
|
|
### Phase 4: pop-search, pop-field, pop-lookup
|
|
|
|
- [ ] pop-search: 필터 조건 입력 (text/date/select/combo)
|
|
- [ ] pop-field: 저장용 입력 (text/number/date/select/numpad)
|
|
- [ ] pop-lookup: 모달 값 선택 (인라인 + 외부 참조)
|
|
|
|
### Phase 5: 고도화
|
|
|
|
- [ ] pop-table 카드 템플릿 디자이너
|
|
|
|
### Phase 6: pop-system
|
|
|
|
- [ ] 프로필, 테마, 대시보드 보이기/숨기기 통합
|
|
|
|
### 후속 작업
|
|
|
|
- [ ] 워크플로우 연동 (버튼 액션, 화면 전환)
|
|
- [ ] 실기기 테스트 (아이폰 SE, iPad Mini 등)
|
|
|
|
**참고 문서**: [POPUPDATE_2.md](../POPUPDATE_2.md) (컴포넌트 정의서 v8.0)
|
|
|
|
---
|
|
|
|
## 현재 구현 계획
|
|
|
|
> **용도**: 이 섹션은 "지금 바로 실행할 구체적 계획"입니다.
|
|
> 새 세션에서 이 섹션만 읽으면 코딩을 시작할 수 있어야 합니다.
|
|
> 완료되면 다음 기능의 계획으로 **교체**합니다.
|
|
|
|
### 대상: Phase 0 공통 인프라 (usePopEvent + useDataSource 훅)
|
|
|
|
#### 배경 (2026-02-11)
|
|
|
|
모든 데이터 연동 POP 컴포넌트(pop-button, pop-table, pop-search 등)가 공유하는 2개 핵심 훅을 구현한다.
|
|
|
|
- **usePopEvent**: 같은 화면(screenId) 안에서 컴포넌트 간 이벤트 통신 (publish/subscribe)
|
|
- **useDataSource**: DB 테이블 CRUD 통합 (조회/생성/수정/삭제)
|
|
|
|
**핵심 원칙**: 새로 만드는 것이 아니라, 기존 코드를 공통화하는 작업이다.
|
|
|
|
- `useDataSource`는 대시보드의 `dataFetcher.ts` 조회 로직 + 기존 `dataApi` CRUD를 훅으로 래핑
|
|
- `usePopEvent`는 신규 구현 (Map 기반 이벤트 버스)
|
|
- 대시보드는 **이번에 교체하지 않는다** (훅 안정화 후 별도 교체)
|
|
|
|
#### 결정사항 (2026-02-11)
|
|
|
|
| 항목 | 결정 | 이유 |
|
|
|------|------|------|
|
|
| usePopEvent 범위 | 같은 screenId 안에서만 통신 | 화면 간 의존성 방지 |
|
|
| usePopEvent 모달 | 같은 screenId면 모달 안 컴포넌트도 통신 가능 | 모달은 별도 화면이 아님 |
|
|
| useDataSource 조회 분기 | 집계/조인이면 SQL 빌더 + executeQuery, 단순이면 dataApi.getTableData | 대시보드 dataFetcher.ts와 동일 전략 |
|
|
| useDataSource CRUD | dataApi.createRecord/updateRecord/deleteRecord 래핑 | 백엔드 API 이미 완성됨 |
|
|
| 대시보드 교체 시점 | 이번에 하지 않음, 훅 안정화 후 별도 작업 | 안정성 우선 |
|
|
| SQL 빌더 위치 | dataFetcher.ts에서 추출하여 별도 유틸로 분리 | 훅과 대시보드 모두 사용 |
|
|
| 이벤트 버스 저장소 | 전역 Map (screenId -> EventEmitter) | React 외부에서도 접근 가능, GC 관리 용이 |
|
|
|
|
---
|
|
|
|
#### 구현 순서 (의존성 기반)
|
|
|
|
| 순서 | 파일 | 작업 | 의존성 | 상태 |
|
|
|------|------|------|--------|------|
|
|
| 1 | `hooks/pop/usePopEvent.ts` | 이벤트 버스 훅 (신규) | 없음 | **완료** |
|
|
| 2 | `hooks/pop/popSqlBuilder.ts` | SQL 빌더 유틸 분리 (dataFetcher.ts에서 추출) | 없음 | **완료** |
|
|
| 3 | `hooks/pop/useDataSource.ts` | 데이터 CRUD 훅 (신규) | 2 | **완료** |
|
|
| 4 | `hooks/pop/index.ts` | 배럴 파일 (re-export) | 1, 3 | **완료** |
|
|
|
|
---
|
|
|
|
#### STEP 1: `usePopEvent.ts` (신규 생성)
|
|
|
|
**파일**: `frontend/hooks/pop/usePopEvent.ts`
|
|
|
|
**역할**: 같은 화면(screenId) 안에서 컴포넌트 간 이벤트 통신
|
|
|
|
**핵심 구조**:
|
|
|
|
```
|
|
전역 저장소 (React 외부)
|
|
screenBuses: Map<string, Map<string, Set<callback>>>
|
|
│
|
|
└── screenId: "S001"
|
|
├── "supplier-selected" → [콜백A, 콜백B]
|
|
├── "data-saved" → [콜백C]
|
|
└── sharedData: Map<string, unknown>
|
|
|
|
sharedDataStore: Map<string, Map<string, unknown>>
|
|
│
|
|
└── screenId: "S001"
|
|
├── "selectedSupplier" → { id: "SUP-001", name: "삼성" }
|
|
└── "inputQuantity" → 50
|
|
```
|
|
|
|
**외부 API (훅이 반환하는 것)**:
|
|
|
|
```typescript
|
|
function usePopEvent(screenId: string) {
|
|
return {
|
|
publish, // (eventName, payload) => void
|
|
subscribe, // (eventName, callback) => unsubscribe 함수
|
|
getSharedData, // (key) => unknown
|
|
setSharedData, // (key, value) => void
|
|
};
|
|
}
|
|
```
|
|
|
|
**상세 구현 명세**:
|
|
|
|
1. **전역 Map 저장소** (모듈 스코프, React 외부)
|
|
- `screenBuses: Map<string, Map<string, Set<Function>>>` - 이벤트 리스너
|
|
- `sharedDataStore: Map<string, Map<string, unknown>>` - 공유 데이터
|
|
|
|
2. **`publish(eventName, payload)`**
|
|
- 해당 screenId의 eventName에 등록된 모든 콜백을 순회하며 payload 전달
|
|
- 등록된 리스너가 없으면 아무 일도 안 함 (에러 아님)
|
|
|
|
3. **`subscribe(eventName, callback)`**
|
|
- 해당 screenId의 eventName에 콜백 등록
|
|
- **반환값**: unsubscribe 함수
|
|
- **useEffect 내부에서 호출되어야 함** (cleanup으로 unsubscribe)
|
|
|
|
4. **`getSharedData(key)`** / **`setSharedData(key, value)`**
|
|
- screenId별 격리된 key-value 저장소
|
|
- publish/subscribe는 "이벤트"(일회성), sharedData는 "상태"(지속)
|
|
- 용도: 버튼이 저장할 때 다른 컴포넌트들의 현재 값을 수집
|
|
|
|
5. **`cleanupScreen(screenId)`** (내부 유틸)
|
|
- 화면 언마운트 시 해당 screenId의 모든 리스너 + sharedData 정리
|
|
- 메모리 누수 방지
|
|
|
|
**사용 예시**:
|
|
|
|
```typescript
|
|
// 거래처 선택 버튼
|
|
const { publish, setSharedData } = usePopEvent("S001");
|
|
|
|
const onSelect = (supplier) => {
|
|
setSharedData("selectedSupplier", supplier); // 상태 저장
|
|
publish("supplier-selected", { supplierId: supplier.id }); // 이벤트 발행
|
|
};
|
|
|
|
// 발주 테이블 (다른 컴포넌트)
|
|
const { subscribe } = usePopEvent("S001");
|
|
|
|
useEffect(() => {
|
|
const unsub = subscribe("supplier-selected", (payload) => {
|
|
refetch({ filters: { supplier_id: payload.supplierId } });
|
|
});
|
|
return unsub; // cleanup
|
|
}, []);
|
|
|
|
// 저장 버튼 (다른 컴포넌트)
|
|
const { getSharedData } = usePopEvent("S001");
|
|
|
|
const handleSave = () => {
|
|
const supplier = getSharedData("selectedSupplier"); // 다른 컴포넌트가 저장한 값 수집
|
|
const quantity = getSharedData("inputQuantity");
|
|
save({ supplier_id: supplier.id, quantity });
|
|
};
|
|
```
|
|
|
|
---
|
|
|
|
#### STEP 2: `popSqlBuilder.ts` (신규 생성 - dataFetcher.ts에서 추출)
|
|
|
|
**파일**: `frontend/hooks/pop/popSqlBuilder.ts`
|
|
|
|
**역할**: DataSourceConfig를 SQL 문자열로 변환하는 순수 유틸리티
|
|
|
|
**기존 dataFetcher.ts에서 그대로 추출할 함수 5개** (로직 변경 없음):
|
|
|
|
| 함수 | 원본 위치 (dataFetcher.ts) | 역할 |
|
|
|------|--------------------------|------|
|
|
| `escapeSQL(value)` | 라인 41~48 | SQL 값 이스케이프 |
|
|
| `sanitizeIdentifier(name)` | 라인 124~127 | 테이블/컬럼명 위험 문자 제거 |
|
|
| `validateDataSourceConfig(config)` | 라인 59~88 | 설정 완료 여부 검증 |
|
|
| `buildWhereClause(filters)` | 라인 93~118 | 필터 -> WHERE 절 변환 |
|
|
| `buildAggregationSQL(config)` | 라인 137~215 | DataSourceConfig -> SELECT SQL 변환 |
|
|
|
|
**export 대상**: `validateDataSourceConfig`, `buildAggregationSQL` (나머지는 내부 함수)
|
|
|
|
**주의**: dataFetcher.ts는 **수정하지 않는다**. 대시보드가 계속 사용 중이므로, 복사만 한다. 대시보드 교체 시점에 dataFetcher.ts에서 이 파일을 import하도록 변경할 예정.
|
|
|
|
---
|
|
|
|
#### STEP 3: `useDataSource.ts` (신규 생성)
|
|
|
|
**파일**: `frontend/hooks/pop/useDataSource.ts`
|
|
|
|
**역할**: DataSourceConfig 기반 DB 테이블 CRUD 통합 훅
|
|
|
|
**내부 의존성**:
|
|
- `popSqlBuilder.ts` - SQL 빌더 (STEP 2)
|
|
- `@/lib/api/data` - dataApi (기존, 조회/생성/수정/삭제)
|
|
- `@/lib/api/dashboard` - dashboardApi (기존, SQL 직접 실행)
|
|
- `@/lib/api/client` - apiClient (기존, axios 기반)
|
|
|
|
**외부 API (훅이 반환하는 것)**:
|
|
|
|
```typescript
|
|
function useDataSource(config: DataSourceConfig) {
|
|
return {
|
|
// 상태
|
|
data: { rows: [], value: 0, total: 0 },
|
|
loading: boolean,
|
|
error: string | null,
|
|
|
|
// 조회
|
|
refetch: (overrideFilters?) => Promise<void>,
|
|
|
|
// 쓰기
|
|
save: (record) => Promise<MutationResult>,
|
|
update: (id, record) => Promise<MutationResult>,
|
|
remove: (id) => Promise<MutationResult>,
|
|
};
|
|
}
|
|
```
|
|
|
|
**MutationResult 타입** (신규):
|
|
|
|
```typescript
|
|
interface MutationResult {
|
|
success: boolean;
|
|
data?: any;
|
|
error?: string;
|
|
}
|
|
```
|
|
|
|
**조회 분기 로직 (핵심)**:
|
|
|
|
```
|
|
config에 aggregation 또는 joins가 있는가?
|
|
├── YES → buildAggregationSQL(config) → apiClient.post("/dashboards/execute-query")
|
|
│ (대시보드와 동일한 경로, SQL 직접 실행)
|
|
│ 실패 시 → dashboardApi.executeQuery() 폴백
|
|
│
|
|
└── NO → dataApi.getTableData(tableName, { page, size, filters, sortBy, sortOrder })
|
|
(단순 테이블 조회)
|
|
```
|
|
|
|
**CRUD 메서드 구현**:
|
|
|
|
| 메서드 | 내부 호출 | 비고 |
|
|
|--------|----------|------|
|
|
| `save(record)` | `dataApi.createRecord(config.tableName, record)` | company_code 자동 추가는 백엔드가 처리 |
|
|
| `update(id, record)` | `dataApi.updateRecord(config.tableName, id, record)` | |
|
|
| `remove(id)` | `dataApi.deleteRecord(config.tableName, id)` | 복합키 객체도 지원 |
|
|
|
|
**자동 새로고침**:
|
|
|
|
```typescript
|
|
useEffect(() => {
|
|
if (config.tableName) refetch();
|
|
|
|
if (config.refreshInterval && config.refreshInterval > 0) {
|
|
const sec = Math.max(5, config.refreshInterval); // 최소 5초
|
|
const timer = setInterval(refetch, sec * 1000);
|
|
return () => clearInterval(timer);
|
|
}
|
|
}, [config.tableName, config.refreshInterval]);
|
|
```
|
|
|
|
**refetch 오버라이드 필터**:
|
|
|
|
```typescript
|
|
// 기본 조회
|
|
refetch();
|
|
|
|
// 필터 추가하여 조회 (usePopEvent와 연동 시)
|
|
refetch({ filters: { supplier_id: "SUP-001" } });
|
|
```
|
|
|
|
내부적으로 `overrideFilters`가 있으면 `config.filters`에 병합하여 조회한다.
|
|
|
|
**사용 예시**:
|
|
|
|
```typescript
|
|
// 대시보드 스타일 (집계)
|
|
const { data, loading } = useDataSource({
|
|
tableName: "sales_order",
|
|
aggregation: { type: "sum", column: "amount", groupBy: ["category"] },
|
|
refreshInterval: 30,
|
|
});
|
|
// data.rows → [{ category: "A", value: 1500 }, ...]
|
|
// data.value → 첫 번째 행의 value
|
|
|
|
// 테이블 스타일 (목록)
|
|
const { data, refetch } = useDataSource({
|
|
tableName: "purchase_order",
|
|
sort: [{ column: "created_at", direction: "desc" }],
|
|
limit: 20,
|
|
});
|
|
// data.rows → [{ id: 1, item_name: "볼트", ... }, ...]
|
|
// data.total → 전체 행 수
|
|
|
|
// 버튼 스타일 (저장만)
|
|
const { save, remove, loading } = useDataSource({
|
|
tableName: "inbound_record",
|
|
});
|
|
const result = await save({ supplier_id: "SUP-001", quantity: 50 });
|
|
// result.success → true/false
|
|
```
|
|
|
|
---
|
|
|
|
#### STEP 4: `index.ts` (신규 생성 - 배럴 파일)
|
|
|
|
**파일**: `frontend/hooks/pop/index.ts`
|
|
|
|
```typescript
|
|
export { usePopEvent, cleanupScreen } from "./usePopEvent";
|
|
export { useDataSource } from "./useDataSource";
|
|
export type { MutationResult } from "./useDataSource";
|
|
export { buildAggregationSQL, validateDataSourceConfig } from "./popSqlBuilder";
|
|
```
|
|
|
|
외부에서 사용할 때: `import { usePopEvent, useDataSource } from "@/hooks/pop";`
|
|
|
|
---
|
|
|
|
#### 사전 충돌 검사 결과 (2026-02-11)
|
|
|
|
| 이름 | 유형 | 검색 범위 | 검색 결과 | 판정 |
|
|
|------|------|-----------|-----------|------|
|
|
| `usePopEvent` | 훅 이름 | frontend 전체 | **주석 2건** (PopDashboardComponent.tsx 라인 10, `@INFRA-EXTRACT` 교체 예정 주석) | **충돌 없음** (실제 코드 아님) |
|
|
| `useDataSource` | 훅 이름 | frontend 전체 | **주석 4건** (dataFetcher.ts 라인 4,227,355 + PopDashboardComponent.tsx 라인 9, 모두 `@INFRA-EXTRACT` 주석) | **충돌 없음** (실제 코드 아님) |
|
|
| `PopEventBus` | 클래스명 | frontend 전체 | **1건** (PopDashboardComponent.tsx 라인 10, `@INFRA-EXTRACT` 주석 내) | **충돌 없음** (주석) |
|
|
| `MutationResult` | 타입명 | frontend 전체 | **0건** | **충돌 없음** |
|
|
| `popSqlBuilder` | 파일명 | frontend 전체 | **0건** | **충돌 없음** |
|
|
| `cleanupScreen` | 함수명 | frontend 전체 | **0건** | **충돌 없음** |
|
|
| `screenBuses` | 변수명 | frontend 전체 | **0건** | **충돌 없음** |
|
|
| `sharedDataStore` | 변수명 | frontend 전체 | **0건** | **충돌 없음** |
|
|
| `buildAggregationSQL` | 함수명 | frontend 전체 | **2건** (dataFetcher.ts 정의 + PopDashboardComponent에서 import) | **충돌 주의**: 동일 이름을 popSqlBuilder.ts에서 재정의. 대시보드는 여전히 dataFetcher.ts 것을 사용하므로 런타임 충돌 없음. 향후 대시보드 교체 시 import 경로만 변경. |
|
|
| `validateDataSourceConfig` | 함수명 | frontend 전체 | **1건** (dataFetcher.ts 정의) | 위와 동일 |
|
|
| `escapeSQL` | 함수명 | frontend 전체 | **1건** (dataFetcher.ts 내부 함수) | **충돌 없음** (export 안 됨) |
|
|
| `sanitizeIdentifier` | 함수명 | frontend 전체 | **1건** (dataFetcher.ts 내부 함수) | **충돌 없음** (export 안 됨) |
|
|
| `AggregatedResult` | 타입명 | frontend 전체 | **1건** (dataFetcher.ts 정의) | **충돌 주의**: useDataSource 내부에서 동일 타입 사용. types.ts 정의를 공유하므로 별도 재정의 불필요. |
|
|
|
|
#### 정의-사용 매핑
|
|
|
|
| 정의 | 정의 위치 | 사용 위치 |
|
|
|------|-----------|-----------|
|
|
| `usePopEvent` | `hooks/pop/usePopEvent.ts` (신규) | pop-button (Phase 2), pop-table (Phase 3), pop-search (Phase 4) 등 |
|
|
| `useDataSource` | `hooks/pop/useDataSource.ts` (신규) | pop-button (Phase 2), pop-table (Phase 3), pop-dashboard (향후 교체) 등 |
|
|
| `MutationResult` | `hooks/pop/useDataSource.ts` (신규) | useDataSource 반환 타입으로 사용 |
|
|
| `buildAggregationSQL` | `hooks/pop/popSqlBuilder.ts` (신규) | useDataSource 내부에서 호출 |
|
|
| `validateDataSourceConfig` | `hooks/pop/popSqlBuilder.ts` (신규) | useDataSource 내부에서 호출 |
|
|
| `cleanupScreen` | `hooks/pop/usePopEvent.ts` (신규) | 화면 언마운트 시 호출 (PopRenderer 또는 뷰어에서) |
|
|
| `DataSourceConfig` | `lib/registry/pop-components/types.ts` (기존) | useDataSource 파라미터 타입 |
|
|
| `DataSourceFilter` | `lib/registry/pop-components/types.ts` (기존) | popSqlBuilder 내부 |
|
|
| `dataApi` | `lib/api/data.ts` (기존) | useDataSource 내부에서 CRUD 호출 |
|
|
| `dashboardApi` | `lib/api/dashboard.ts` (기존) | useDataSource 내부에서 SQL 실행 폴백 |
|
|
| `apiClient` | `lib/api/client.ts` (기존) | useDataSource 내부에서 SQL 실행 1차 |
|
|
|
|
**누락 검사**: 모든 신규 정의에 사용처 있음. 모든 사용처에 정의 존재.
|
|
단, `cleanupScreen`의 호출 시점은 Phase 2 이후 뷰어 통합 시 결정 (이번에는 export만 해둠).
|
|
|
|
---
|
|
|
|
#### 함정 경고
|
|
|
|
| 번호 | 위험 | 설명 | 해결 방안 |
|
|
|------|------|------|-----------|
|
|
| W1 | **subscribe를 useEffect 밖에서 호출하면 메모리 누수** | subscribe는 콜백을 등록하므로, 컴포넌트 언마운트 시 해제해야 함 | subscribe의 반환값(unsubscribe)을 useEffect cleanup에서 호출. JSDoc에 사용 패턴 명시. |
|
|
| W2 | **DataSourceConfig의 import 경로** | `DataSourceConfig`는 `lib/registry/pop-components/types.ts`에 정의됨. hooks 디렉토리에서 import 시 경로가 김 | `@/lib/registry/pop-components/types`로 import. 별도 re-export 하지 않음 (타입 중복 방지). |
|
|
| W3 | **buildAggregationSQL 동일 이름 2곳** | dataFetcher.ts와 popSqlBuilder.ts에 같은 이름의 함수가 존재 | 의도적 복사. 대시보드는 dataFetcher.ts, 새 컴포넌트는 popSqlBuilder.ts 사용. 향후 대시보드 교체 시 dataFetcher.ts를 popSqlBuilder.ts import로 변경. |
|
|
| W4 | **apiClient.post와 dashboardApi.executeQuery 이중 경로** | 대시보드 dataFetcher.ts에서 apiClient 우선 + dashboardApi 폴백 패턴을 그대로 복사함 | 동일 패턴 유지 (안정성 검증 완료). 향후 하나로 통합 가능. |
|
|
| W5 | **refetch overrideFilters와 config.filters 병합 순서** | overrideFilters가 config.filters를 완전 대체하는지, 추가하는지 모호 | **추가(append) 방식**: config.filters + overrideFilters를 합침. overrideFilters에 같은 column이 있으면 덮어씀. 이 동작을 JSDoc에 명시. |
|
|
| W6 | **SSR 환경에서 전역 Map** | Next.js SSR에서 전역 Map이 서버/클라이언트 간 공유될 수 있음 | `typeof window !== "undefined"` 가드. 이벤트 버스는 클라이언트 전용. |
|
|
| W7 | **hooks/pop/ 디렉토리 신규** | `frontend/hooks/pop/` 디렉토리가 존재하지 않음 | STEP 1에서 파일 생성 시 디렉토리 자동 생성됨. 수동으로 mkdir 불필요. |
|
|
|
|
---
|
|
|
|
#### 작업 완료 후 확인 체크리스트
|
|
|
|
##### 코드 레벨
|
|
|
|
- [ ] `usePopEvent` - publish/subscribe 기본 동작 (같은 screenId)
|
|
- [ ] `usePopEvent` - 다른 screenId 간 격리 확인
|
|
- [ ] `usePopEvent` - subscribe cleanup (메모리 누수 없음)
|
|
- [ ] `usePopEvent` - sharedData set/get
|
|
- [ ] `useDataSource` - 단순 조회 (aggregation 없음 → dataApi.getTableData)
|
|
- [ ] `useDataSource` - 집계 조회 (aggregation 있음 → SQL 빌더 → executeQuery)
|
|
- [ ] `useDataSource` - save/update/remove
|
|
- [ ] `useDataSource` - loading/error 상태 관리
|
|
- [ ] `useDataSource` - refreshInterval 자동 새로고침
|
|
- [ ] `popSqlBuilder` - buildAggregationSQL이 dataFetcher.ts와 동일 결과 생성
|
|
- [ ] TypeScript 컴파일 에러 0건
|
|
- [ ] 린트 에러 0건
|
|
- [ ] 기존 대시보드 동작에 영향 없음 (dataFetcher.ts 미수정)
|
|
|
|
##### 구조 레벨
|
|
|
|
- [ ] `frontend/hooks/pop/` 디렉토리 생성됨
|
|
- [ ] index.ts 배럴 파일에서 모든 public API export 됨
|
|
- [ ] `@/hooks/pop`으로 import 가능
|
|
|
|
---
|
|
|
|
#### 이전 완료된 계획 (보관)
|
|
|
|
**대시보드 스타일 정리 (2026-02-11, 완료)**:
|
|
글자 크기 커스텀 제거, 라벨 정렬만 유지, stale closure 수정, .next 캐시 해결.
|
|
상세: `popdocs/sessions/2026-02-11.md`
|
|
|
|
**브라우저 확인 체크리스트 (대기)**:
|
|
- [ ] 라벨 정렬, 페이지 미리보기, 차트 디자인, 게이지/통계카드 동작 확인
|
|
|
|
---
|
|
|
|
## 브레이크포인트 (v5.2 현재)
|
|
|
|
| 모드 | 화면 너비 | 칸 수 | 대상 기기 |
|
|
|------|----------|-------|----------|
|
|
| mobile_portrait | ~479px | 4칸 | 아이폰 SE ~ 갤럭시 S |
|
|
| mobile_landscape | 480~767px | 6칸 | 스마트폰 가로 |
|
|
| tablet_portrait | 768~1023px | 8칸 | 8~10인치 태블릿 세로 |
|
|
| tablet_landscape | 1024px~ | 12칸 | 10~14인치 태블릿 가로 |
|
|
|
|
---
|
|
|
|
## 관련 문서
|
|
|
|
| 문서 | 내용 |
|
|
|------|------|
|
|
| [STATUS.md](./STATUS.md) | 현재 진행 상태 |
|
|
| [SPEC.md](./SPEC.md) | 기술 스펙 |
|
|
| [ARCHITECTURE.md](./ARCHITECTURE.md) | 코드 구조 |
|
|
| [POPUPDATE_2.md](../POPUPDATE_2.md) | 컴포넌트 정의서 v8.0 (최신) |
|
|
| [components-spec.md](./components-spec.md) | 컴포넌트 상세 설계 (v4 기준, 갱신 필요) |
|
|
| [decisions/005](./decisions/005-breakpoint-redesign.md) | 브레이크포인트 재설계 ADR |
|
|
|
|
---
|
|
|
|
*최종 업데이트: 2026-02-11 (Phase 0 공통 인프라 완료, Phase 2 pop-button 설계 시작)*
|