23 KiB
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 관련 파일은 모두 삭제되었습니다.
- v4 기본 구조, 렌더러, 디자이너 통합
- Undo/Redo, 드래그 리사이즈, Flexbox 가로 배치
- 비율 스케일링 시스템
- 오버라이드 기능 (모드별 배치 고정)
- 컴포넌트 표시/숨김, 줄바꿈
Phase 5: v5 CSS Grid 시스템 (완료)
Phase 5.1: 타입 정의 (완료)
PopLayoutDataV5인터페이스PopGridConfig,PopGridPosition타입GridMode,GRID_BREAKPOINTS상수createEmptyPopLayoutV5(),isV5Layout(),detectGridMode()
Phase 5.2: 그리드 렌더러 (완료)
PopRenderer.tsx- CSS Grid 기반 렌더링- 격자 셀 렌더링 (CSS Grid 동일 좌표계)
- 위치 변환 (12칸 -> 4/6/8칸)
Phase 5.3: 디자이너 UI (완료)
PopCanvas.tsx- 그리드 캔버스 + 행/열 라벨- 드래그 스냅 (칸에 맞춤)
ComponentEditorPanel.tsx- 위치 편집
Phase 5.4: 반응형 자동화 (완료)
- 자동 변환 알고리즘 (12칸 -> 4칸)
- 겹침 감지 및 재배치
- 모드별 오버라이드 저장
v5.1 추가 기능 (완료)
- 자동 줄바꿈 (col > maxCol -> 맨 아래 배치)
- "검토 필요" 알림 시스템
- Gap 프리셋 (좁게/보통/넓게)
- 숨김 기능 (모드별)
v5.2 브레이크포인트 재설계 + 세로 자동 확장 (완료)
- 기기 기반 브레이크포인트 (479/767/1023px)
- 세로 자동 확장 (dynamicCanvasHeight)
- 뷰어 반응형 일관성 (detectGridMode 사용)
- VIEWPORT_PRESETS에서 height 제거
컴포넌트 설계 (완료)
- 9개 컴포넌트 정의 (POPUPDATE_2.md v8.0)
- POP 헌법 9조 작성
- 공통 인프라 설계 (DataSourceConfig, ColumnBinding, JoinConfig, useDataSource, usePopEvent, PopActionConfig)
- 모달 화면 설계 방식 확정 (인라인 + 외부 참조)
- 기존 시스템 호환성 검증 (DB/백엔드/프론트 변경 불필요 확인)
다음 작업
Phase 0: 공통 인프라
모든 데이터 연동 컴포넌트가 공유하는 기반 시스템:
- ColumnBinding 타입 정의 (read/write/readwrite/hidden) -- types.ts에 추가 완료
- JoinConfig 타입 정의 (테이블 조인) -- types.ts에 추가 완료
- DataSourceConfig 타입 정의 (데이터 소스 설정) -- types.ts에 추가 완료
- PopActionConfig 타입 정의 (액션 설정) -- types.ts에 추가 완료
- usePopEvent 훅 구현 (이벤트 버스, 데이터 전달, 화면 단위 격리) -- 완료
- useDataSource 훅 구현 (CRUD 포함, 기존 dataApi 활용) -- 완료
Phase 1: pop-dashboard -- 완료
2026-02-10: 17단계 코딩 + 검수 + 팔레트 등록 완료
- PopDashboardConfig, DashboardItem 타입 정의
- 멀티 아이템 컨테이너 구현 (여러 아이템 묶음)
- 4개 서브타입: kpi-card, chart, gauge, stat-card
- 4개 표시 모드: arrows, auto-slide, grid, scroll
- 계산식(formula) 지원: "생산량/총재고량" 같은 복합 표현
- 설정 패널: 드롭다운 기반 쉬운 집계 설정 (SQL 불필요)
- 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 (컴포넌트 정의서 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조회 로직 + 기존dataApiCRUD를 훅으로 래핑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 (훅이 반환하는 것):
function usePopEvent(screenId: string) {
return {
publish, // (eventName, payload) => void
subscribe, // (eventName, callback) => unsubscribe 함수
getSharedData, // (key) => unknown
setSharedData, // (key, value) => void
};
}
상세 구현 명세:
-
전역 Map 저장소 (모듈 스코프, React 외부)
screenBuses: Map<string, Map<string, Set<Function>>>- 이벤트 리스너sharedDataStore: Map<string, Map<string, unknown>>- 공유 데이터
-
publish(eventName, payload)- 해당 screenId의 eventName에 등록된 모든 콜백을 순회하며 payload 전달
- 등록된 리스너가 없으면 아무 일도 안 함 (에러 아님)
-
subscribe(eventName, callback)- 해당 screenId의 eventName에 콜백 등록
- 반환값: unsubscribe 함수
- useEffect 내부에서 호출되어야 함 (cleanup으로 unsubscribe)
-
getSharedData(key)/setSharedData(key, value)- screenId별 격리된 key-value 저장소
- publish/subscribe는 "이벤트"(일회성), sharedData는 "상태"(지속)
- 용도: 버튼이 저장할 때 다른 컴포넌트들의 현재 값을 수집
-
cleanupScreen(screenId)(내부 유틸)- 화면 언마운트 시 해당 screenId의 모든 리스너 + sharedData 정리
- 메모리 누수 방지
사용 예시:
// 거래처 선택 버튼
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 (훅이 반환하는 것):
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 타입 (신규):
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) |
복합키 객체도 지원 |
자동 새로고침:
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 오버라이드 필터:
// 기본 조회
refetch();
// 필터 추가하여 조회 (usePopEvent와 연동 시)
refetch({ filters: { supplier_id: "SUP-001" } });
내부적으로 overrideFilters가 있으면 config.filters에 병합하여 조회한다.
사용 예시:
// 대시보드 스타일 (집계)
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
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/getuseDataSource- 단순 조회 (aggregation 없음 → dataApi.getTableData)useDataSource- 집계 조회 (aggregation 있음 → SQL 빌더 → executeQuery)useDataSource- save/update/removeuseDataSource- 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 | 현재 진행 상태 |
| SPEC.md | 기술 스펙 |
| ARCHITECTURE.md | 코드 구조 |
| POPUPDATE_2.md | 컴포넌트 정의서 v8.0 (최신) |
| components-spec.md | 컴포넌트 상세 설계 (v4 기준, 갱신 필요) |
| decisions/005 | 브레이크포인트 재설계 ADR |
최종 업데이트: 2026-02-11 (Phase 0 공통 인프라 완료, Phase 2 pop-button 설계 시작)