Merge branch 'jskim-node' into ycshin-node
- 충돌 해결: TableListComponent.tsx - 양쪽 import 유지 (useTabId + getAdaptiveLabelColor) - buttonColor에 다크모드 대응 함수 적용 Made-with: Cursor
This commit is contained in:
@@ -0,0 +1,155 @@
|
||||
# WACE 반응형 컴포넌트 전략
|
||||
|
||||
## 개요
|
||||
|
||||
WACE 프로젝트의 모든 반응형 UI는 **3개의 레이아웃 프리미티브 + 1개의 훅**으로 통일한다.
|
||||
컴포넌트마다 새로 타입을 정의하거나 리사이저를 구현하지 않는다.
|
||||
|
||||
## 아키텍처
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ useResponsive() 훅 │
|
||||
│ isMobile | isTablet | isDesktop | width │
|
||||
└──────────┬──────────┬──────────┬────────────────┘
|
||||
│ │ │
|
||||
┌───────▼──┐ ┌────▼─────┐ ┌─▼──────────────┐
|
||||
│ 데이터 │ │ 좌우분할 │ │ 캔버스(디자이너)│
|
||||
│ 목록 │ │ 패널 │ │ 화면 │
|
||||
└──────────┘ └──────────┘ └────────────────┘
|
||||
ResponsiveDataView ResponsiveSplitPanel ResponsiveGridRenderer
|
||||
```
|
||||
|
||||
## 1. useResponsive (훅)
|
||||
|
||||
**위치**: `frontend/lib/hooks/useResponsive.ts`
|
||||
|
||||
모든 반응형 판단의 기반. 직접 breakpoint 분기가 필요할 때만 사용.
|
||||
가능하면 아래 레이아웃 컴포넌트를 쓰고, 훅 직접 사용은 최소화.
|
||||
|
||||
| 반환값 | 브레이크포인트 | 해상도 |
|
||||
|--------|---------------|--------|
|
||||
| isMobile | xs, sm | < 768px |
|
||||
| isTablet | md | 768 ~ 1023px |
|
||||
| isDesktop | lg, xl, 2xl | >= 1024px |
|
||||
|
||||
## 2. ResponsiveDataView (데이터 목록)
|
||||
|
||||
**위치**: `frontend/components/common/ResponsiveDataView.tsx`
|
||||
**패턴**: 데스크톱 = 테이블, 모바일 = 카드 리스트
|
||||
**적용 대상**: 모든 목록/리스트 화면
|
||||
|
||||
```tsx
|
||||
<ResponsiveDataView<User>
|
||||
data={users}
|
||||
columns={columns}
|
||||
keyExtractor={(u) => u.id}
|
||||
cardTitle={(u) => u.name}
|
||||
cardFields={[
|
||||
{ label: "이메일", render: (u) => u.email },
|
||||
{ label: "부서", render: (u) => u.dept },
|
||||
]}
|
||||
renderActions={(u) => <Button>편집</Button>}
|
||||
/>
|
||||
```
|
||||
|
||||
**적용 완료 (12개 화면)**:
|
||||
- UserTable, CompanyTable, UserAuthTable
|
||||
- DataFlowList, ScreenList
|
||||
- system-notices, approvalTemplate, standards
|
||||
- batch-management, mail/receive, flowMgmtList
|
||||
- exconList, exCallConfList
|
||||
|
||||
## 3. ResponsiveSplitPanel (좌우 분할)
|
||||
|
||||
**위치**: `frontend/components/common/ResponsiveSplitPanel.tsx`
|
||||
**패턴**: 데스크톱 = 좌우 분할(리사이저 포함), 모바일 = 세로 스택(접기/펼치기)
|
||||
**적용 대상**: 카테고리관리, 메뉴관리, 부서관리, BOM 등 좌우 분할 레이아웃
|
||||
|
||||
```tsx
|
||||
<ResponsiveSplitPanel
|
||||
left={<TreeView />}
|
||||
right={<DetailPanel />}
|
||||
leftTitle="카테고리"
|
||||
leftWidth={25}
|
||||
minLeftWidth={10}
|
||||
maxLeftWidth={40}
|
||||
height="calc(100vh - 120px)"
|
||||
/>
|
||||
```
|
||||
|
||||
**Props**:
|
||||
| Prop | 타입 | 기본값 | 설명 |
|
||||
|------|------|--------|------|
|
||||
| left | ReactNode | 필수 | 좌측 패널 콘텐츠 |
|
||||
| right | ReactNode | 필수 | 우측 패널 콘텐츠 |
|
||||
| leftTitle | string | "목록" | 모바일 접기 헤더 |
|
||||
| leftWidth | number | 25 | 초기 좌측 너비(%) |
|
||||
| minLeftWidth | number | 10 | 최소 좌측 너비(%) |
|
||||
| maxLeftWidth | number | 50 | 최대 좌측 너비(%) |
|
||||
| showResizer | boolean | true | 리사이저 표시 |
|
||||
| collapsedOnMobile | boolean | true | 모바일 기본 접힘 |
|
||||
| height | string | "100%" | 컨테이너 높이 |
|
||||
|
||||
**동작**:
|
||||
- 데스크톱(>= 1024px): 좌우 분할 + 드래그 리사이저 + 좌측 접기 버튼
|
||||
- 모바일(< 1024px): 세로 스택, 좌측 패널 40vh 제한, 접기/펼치기
|
||||
|
||||
**마이그레이션 후보**:
|
||||
- `V2CategoryManagerComponent` (완료)
|
||||
- `SplitPanelLayoutComponent` (v1, v2)
|
||||
- `BomTreeComponent`
|
||||
- `ScreenSplitPanel`
|
||||
- menu/page.tsx (메뉴 관리)
|
||||
- departments/page.tsx (부서 관리)
|
||||
|
||||
## 4. ResponsiveGridRenderer (디자이너 캔버스)
|
||||
|
||||
**위치**: `frontend/components/screen/ResponsiveGridRenderer.tsx`
|
||||
**패턴**: 데스크톱(비전폭 컴포넌트) = 캔버스 스케일링, 그 외 = Flex 그리드
|
||||
**적용 대상**: 화면 디자이너로 만든 동적 화면
|
||||
|
||||
이 컴포넌트는 화면 디자이너 시스템 전용. 일반 개발에서 직접 사용하지 않음.
|
||||
|
||||
## 사용 가이드
|
||||
|
||||
### 새 화면 만들 때
|
||||
|
||||
| 화면 유형 | 사용 컴포넌트 |
|
||||
|-----------|--------------|
|
||||
| 데이터 목록 (테이블) | `ResponsiveDataView` |
|
||||
| 좌우 분할 (트리+상세) | `ResponsiveSplitPanel` |
|
||||
| 디자이너 화면 | `ResponsiveGridRenderer` (자동) |
|
||||
| 단순 레이아웃 | Tailwind 반응형 (`flex-col lg:flex-row`) |
|
||||
|
||||
### 금지 사항
|
||||
|
||||
1. 컴포넌트 내부에 `isDraggingRef`, `handleMouseDown/Move/Up` 직접 구현 금지
|
||||
-> `ResponsiveSplitPanel` 사용
|
||||
2. `hidden lg:block` / `lg:hidden` 패턴으로 테이블/카드 이중 렌더링 금지
|
||||
-> `ResponsiveDataView` 사용
|
||||
3. `window.innerWidth` 직접 체크 금지
|
||||
-> `useResponsive()` 훅 사용
|
||||
4. 반응형 분기를 위한 새로운 타입/인터페이스 정의 금지
|
||||
-> 기존 프리미티브의 Props 사용
|
||||
|
||||
### 폐기 예정 컴포넌트
|
||||
|
||||
| 컴포넌트 | 대체 | 상태 |
|
||||
|----------|------|------|
|
||||
| `ResponsiveContainer` | Tailwind 또는 `useResponsive` | 미사용, 삭제 예정 |
|
||||
| `ResponsiveGrid` | Tailwind `grid-cols-*` | 미사용, 삭제 예정 |
|
||||
| `ResponsiveText` | Tailwind `text-sm lg:text-lg` | 미사용, 삭제 예정 |
|
||||
|
||||
## 파일 구조
|
||||
|
||||
```
|
||||
frontend/
|
||||
├── lib/hooks/
|
||||
│ └── useResponsive.ts # 브레이크포인트 훅 (기반)
|
||||
├── components/common/
|
||||
│ ├── ResponsiveDataView.tsx # 테이블/카드 전환
|
||||
│ └── ResponsiveSplitPanel.tsx # 좌우 분할 반응형
|
||||
└── components/screen/
|
||||
└── ResponsiveGridRenderer.tsx # 디자이너 캔버스 렌더러
|
||||
```
|
||||
@@ -0,0 +1,380 @@
|
||||
# 결재 시스템 v2 사용 가이드
|
||||
|
||||
## 개요
|
||||
|
||||
결재 시스템 v2는 기존 순차결재(escalation) 외에 다양한 결재 방식을 지원합니다.
|
||||
|
||||
| 결재 유형 | 코드 | 설명 |
|
||||
|-----------|------|------|
|
||||
| 순차결재 (기본) | `escalation` | 결재선 순서대로 한 명씩 처리 |
|
||||
| 전결 (자기결재) | `self` | 상신자 본인이 직접 승인 (결재자 불필요) |
|
||||
| 합의결재 | `consensus` | 같은 단계에 여러 결재자 → 전원 승인 필요 |
|
||||
| 후결 | `post` | 먼저 실행 후 나중에 결재 (결재 전 상태에서도 업무 진행) |
|
||||
|
||||
추가 기능:
|
||||
- **대결 위임**: 부재 시 다른 사용자에게 결재 위임
|
||||
- **통보 단계**: 결재선에 통보만 하는 단계 (자동 승인 처리)
|
||||
- **긴급도**: `normal` / `urgent` / `critical`
|
||||
- **혼합형 결재선**: 한 결재선에 결재/합의/통보 단계를 자유롭게 조합
|
||||
|
||||
---
|
||||
|
||||
## DB 스키마 변경사항
|
||||
|
||||
### 마이그레이션 적용
|
||||
|
||||
```bash
|
||||
# 개발 DB에 마이그레이션 적용
|
||||
psql -h 39.117.244.52 -p 11132 -U postgres -d plm -f db/migrations/1051_approval_system_v2.sql
|
||||
psql -h 39.117.244.52 -p 11132 -U postgres -d plm -f db/migrations/1052_rename_proxy_id_to_id.sql
|
||||
```
|
||||
|
||||
### 변경된 테이블
|
||||
|
||||
#### approval_requests (추가 컬럼)
|
||||
|
||||
| 컬럼 | 타입 | 기본값 | 설명 |
|
||||
|------|------|--------|------|
|
||||
| approval_type | VARCHAR(20) | 'escalation' | self/escalation/consensus/post |
|
||||
| is_post_approved | BOOLEAN | FALSE | 후결 처리 완료 여부 |
|
||||
| post_approved_at | TIMESTAMPTZ | NULL | 후결 처리 시각 |
|
||||
| urgency | VARCHAR(20) | 'normal' | normal/urgent/critical |
|
||||
|
||||
#### approval_lines (추가 컬럼)
|
||||
|
||||
| 컬럼 | 타입 | 기본값 | 설명 |
|
||||
|------|------|--------|------|
|
||||
| step_type | VARCHAR(20) | 'approval' | approval/consensus/notification |
|
||||
| proxy_for | VARCHAR(50) | NULL | 대결 시 원래 결재자 ID |
|
||||
| proxy_reason | TEXT | NULL | 대결 사유 |
|
||||
| is_required | BOOLEAN | TRUE | 필수 결재 여부 |
|
||||
|
||||
#### approval_proxy_settings (신규)
|
||||
|
||||
| 컬럼 | 타입 | 설명 |
|
||||
|------|------|------|
|
||||
| id | SERIAL PK | |
|
||||
| company_code | VARCHAR(20) NOT NULL | |
|
||||
| original_user_id | VARCHAR(50) | 원래 결재자 |
|
||||
| proxy_user_id | VARCHAR(50) | 대결자 |
|
||||
| start_date | DATE | 위임 시작일 |
|
||||
| end_date | DATE | 위임 종료일 |
|
||||
| reason | TEXT | 위임 사유 |
|
||||
| is_active | CHAR(1) | 'Y'/'N' |
|
||||
|
||||
---
|
||||
|
||||
## API 엔드포인트
|
||||
|
||||
모든 API는 `/api/approval` 접두사 + JWT 인증 필수.
|
||||
|
||||
### 결재 요청 (Requests)
|
||||
|
||||
| Method | Endpoint | 설명 |
|
||||
|--------|----------|------|
|
||||
| GET | `/requests` | 목록 조회 |
|
||||
| GET | `/requests/:id` | 상세 조회 (lines 포함) |
|
||||
| POST | `/requests` | 결재 요청 생성 |
|
||||
| POST | `/requests/:id/cancel` | 결재 회수 |
|
||||
| POST | `/requests/:id/post-approve` | 후결 처리 |
|
||||
|
||||
#### 결재 요청 생성 Body
|
||||
|
||||
```typescript
|
||||
{
|
||||
title: string;
|
||||
target_table: string;
|
||||
target_record_id: string;
|
||||
approval_type?: "self" | "escalation" | "consensus" | "post"; // 기본: escalation
|
||||
urgency?: "normal" | "urgent" | "critical"; // 기본: normal
|
||||
definition_id?: number;
|
||||
target_record_data?: Record<string, any>;
|
||||
approvers: Array<{
|
||||
approver_id: string;
|
||||
step_order: number;
|
||||
step_type?: "approval" | "consensus" | "notification"; // 기본: approval
|
||||
}>;
|
||||
}
|
||||
```
|
||||
|
||||
#### 결재 유형별 요청 예시
|
||||
|
||||
**전결 (self)**: 결재자 없이 본인 즉시 승인
|
||||
|
||||
```typescript
|
||||
await createApprovalRequest({
|
||||
title: "긴급 출장비 전결",
|
||||
target_table: "expense",
|
||||
target_record_id: "123",
|
||||
approval_type: "self",
|
||||
approvers: [],
|
||||
});
|
||||
```
|
||||
|
||||
**합의결재 (consensus)**: 같은 step_order에 여러 결재자
|
||||
|
||||
```typescript
|
||||
await createApprovalRequest({
|
||||
title: "프로젝트 예산안 합의",
|
||||
target_table: "budget",
|
||||
target_record_id: "456",
|
||||
approval_type: "consensus",
|
||||
approvers: [
|
||||
{ approver_id: "user1", step_order: 1, step_type: "consensus" },
|
||||
{ approver_id: "user2", step_order: 1, step_type: "consensus" },
|
||||
{ approver_id: "user3", step_order: 1, step_type: "consensus" },
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
**혼합형 결재선**: 결재 → 합의 → 통보 조합
|
||||
|
||||
```typescript
|
||||
await createApprovalRequest({
|
||||
title: "신규 채용 승인",
|
||||
target_table: "recruitment",
|
||||
target_record_id: "789",
|
||||
approval_type: "escalation",
|
||||
approvers: [
|
||||
{ approver_id: "teamLead", step_order: 1, step_type: "approval" },
|
||||
{ approver_id: "hrManager", step_order: 2, step_type: "consensus" },
|
||||
{ approver_id: "cfo", step_order: 2, step_type: "consensus" },
|
||||
{ approver_id: "ceo", step_order: 3, step_type: "approval" },
|
||||
{ approver_id: "secretary", step_order: 4, step_type: "notification" },
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
**후결 (post)**: 먼저 실행 후 나중에 결재
|
||||
|
||||
```typescript
|
||||
await createApprovalRequest({
|
||||
title: "긴급 자재 발주",
|
||||
target_table: "purchase_order",
|
||||
target_record_id: "101",
|
||||
approval_type: "post",
|
||||
urgency: "urgent",
|
||||
approvers: [
|
||||
{ approver_id: "manager", step_order: 1, step_type: "approval" },
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
### 결재 처리 (Lines)
|
||||
|
||||
| Method | Endpoint | 설명 |
|
||||
|--------|----------|------|
|
||||
| GET | `/my-pending` | 내 결재 대기 목록 |
|
||||
| POST | `/lines/:lineId/process` | 승인/반려 처리 |
|
||||
|
||||
#### 승인/반려 Body
|
||||
|
||||
```typescript
|
||||
{
|
||||
action: "approved" | "rejected";
|
||||
comment?: string;
|
||||
proxy_reason?: string; // 대결 시 사유
|
||||
}
|
||||
```
|
||||
|
||||
대결 처리: 원래 결재자가 아닌 사용자가 처리하면 자동으로 대결 설정 확인 후 `proxy_for`, `proxy_reason` 기록.
|
||||
|
||||
### 대결 위임 설정 (Proxy Settings)
|
||||
|
||||
| Method | Endpoint | 설명 |
|
||||
|--------|----------|------|
|
||||
| GET | `/proxy-settings` | 위임 목록 |
|
||||
| POST | `/proxy-settings` | 위임 생성 |
|
||||
| PUT | `/proxy-settings/:id` | 위임 수정 |
|
||||
| DELETE | `/proxy-settings/:id` | 위임 삭제 |
|
||||
| GET | `/proxy-settings/check/:userId` | 활성 대결자 확인 |
|
||||
|
||||
#### 대결 생성 Body
|
||||
|
||||
```typescript
|
||||
{
|
||||
original_user_id: string;
|
||||
proxy_user_id: string;
|
||||
start_date: string; // "2026-03-10"
|
||||
end_date: string; // "2026-03-20"
|
||||
reason?: string;
|
||||
is_active?: "Y" | "N";
|
||||
}
|
||||
```
|
||||
|
||||
### 템플릿 (Templates)
|
||||
|
||||
| Method | Endpoint | 설명 |
|
||||
|--------|----------|------|
|
||||
| GET | `/templates` | 템플릿 목록 |
|
||||
| GET | `/templates/:id` | 템플릿 상세 (steps 포함) |
|
||||
| POST | `/templates` | 템플릿 생성 |
|
||||
| PUT | `/templates/:id` | 템플릿 수정 |
|
||||
| DELETE | `/templates/:id` | 템플릿 삭제 |
|
||||
|
||||
---
|
||||
|
||||
## 프론트엔드 화면
|
||||
|
||||
### 1. 결재 요청 모달 (`ApprovalRequestModal`)
|
||||
|
||||
경로: `frontend/components/approval/ApprovalRequestModal.tsx`
|
||||
|
||||
- 결재 유형 선택: 상신결재 / 전결 / 합의결재 / 후결
|
||||
- 템플릿 불러오기: 등록된 템플릿에서 결재선 자동 세팅
|
||||
- 전결 시 결재자 섹션 숨김 + "본인이 직접 승인합니다" 안내
|
||||
- 합의결재 시 결재자 레이블 "합의 결재자"로 변경
|
||||
- 후결 시 안내 배너 표시
|
||||
- 혼합형 step_type 뱃지 표시 (결재/합의/통보)
|
||||
|
||||
### 2. 결재함 (`/admin/approvalBox`)
|
||||
|
||||
경로: `frontend/app/(main)/admin/approvalBox/page.tsx`
|
||||
|
||||
탭 구성:
|
||||
- **수신함**: 내가 결재할 건 목록
|
||||
- **상신함**: 내가 요청한 건 목록
|
||||
- **대결 설정**: 대결 위임 CRUD
|
||||
|
||||
대결 설정 기능:
|
||||
- 위임자/대결자 사용자 검색 (디바운스 300ms)
|
||||
- 시작일/종료일 설정
|
||||
- 활성/비활성 토글
|
||||
- 기간 중복 체크 (서버 측)
|
||||
- 등록/수정/삭제 모달
|
||||
|
||||
### 3. 결재 템플릿 관리 (`/admin/approvalTemplate`)
|
||||
|
||||
경로: `frontend/app/(main)/admin/approvalTemplate/page.tsx`
|
||||
|
||||
- 템플릿 목록/검색
|
||||
- 등록/수정 Dialog
|
||||
- 단계별 결재 유형 설정 (결재/합의/통보)
|
||||
- 합의 단계: "합의자 추가" 버튼으로 같은 step_order에 복수 결재자
|
||||
- 결재자 사용자 검색
|
||||
|
||||
### 4. 결재 단계 컴포넌트 (`v2-approval-step`)
|
||||
|
||||
경로: `frontend/lib/registry/components/v2-approval-step/`
|
||||
|
||||
화면 디자이너에서 사용하는 결재 단계 시각화 컴포넌트:
|
||||
- 가로형/세로형 스테퍼
|
||||
- step_order 기준 그룹핑 (합의결재 시 가로 나열)
|
||||
- step_type 아이콘: 결재(CheckCircle), 합의(Users), 통보(Bell)
|
||||
- 상태별 색상: 승인(success), 반려(destructive), 대기(warning)
|
||||
- 대결/후결/전결 뱃지
|
||||
- 긴급도 표시 (urgent: 주황 dot, critical: 빨강 배경)
|
||||
|
||||
---
|
||||
|
||||
## API 클라이언트 사용법
|
||||
|
||||
```typescript
|
||||
import {
|
||||
// 결재 요청
|
||||
createApprovalRequest,
|
||||
getApprovalRequests,
|
||||
getApprovalRequest,
|
||||
cancelApprovalRequest,
|
||||
postApproveRequest,
|
||||
|
||||
// 대결 위임
|
||||
getProxySettings,
|
||||
createProxySetting,
|
||||
updateProxySetting,
|
||||
deleteProxySetting,
|
||||
checkActiveProxy,
|
||||
|
||||
// 템플릿 단계
|
||||
getTemplateSteps,
|
||||
createTemplateStep,
|
||||
updateTemplateStep,
|
||||
deleteTemplateStep,
|
||||
|
||||
// 타입
|
||||
type ApprovalProxySetting,
|
||||
type CreateApprovalRequestInput,
|
||||
type ApprovalLineTemplateStep,
|
||||
} from "@/lib/api/approval";
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 핵심 로직 설명
|
||||
|
||||
### 동시성 보호 (FOR UPDATE)
|
||||
|
||||
결재 처리(`processApproval`)에서 동시 승인/반려 방지:
|
||||
|
||||
```sql
|
||||
SELECT * FROM approval_lines WHERE line_id = $1 FOR UPDATE
|
||||
SELECT * FROM approval_requests WHERE request_id = $1 FOR UPDATE
|
||||
```
|
||||
|
||||
### 대결 자동 감지
|
||||
|
||||
결재자가 아닌 사용자가 결재 처리하면:
|
||||
1. `approval_proxy_settings`에서 활성 대결 설정 확인
|
||||
2. 대결 설정이 있으면 → `proxy_for`, `proxy_reason` 자동 기록
|
||||
3. 없으면 → 403 에러
|
||||
|
||||
### 통보 단계 자동 처리
|
||||
|
||||
`step_type = 'notification'`인 단계가 활성화되면:
|
||||
1. 해당 단계의 모든 결재자를 자동 `approved` 처리
|
||||
2. `comment = '자동 통보 처리'` 기록
|
||||
3. `activateNextStep()` 재귀 호출로 다음 단계 진행
|
||||
|
||||
### 합의결재 단계 완료 판정
|
||||
|
||||
같은 `step_order`의 모든 결재자가 `approved`여야 다음 단계로:
|
||||
|
||||
```sql
|
||||
SELECT COUNT(*) FROM approval_lines
|
||||
WHERE request_id = $1 AND step_order = $2
|
||||
AND status NOT IN ('approved', 'skipped')
|
||||
```
|
||||
|
||||
하나라도 `rejected`면 전체 결재 반려.
|
||||
|
||||
---
|
||||
|
||||
## 메뉴 등록
|
||||
|
||||
결재 관련 화면을 메뉴에 등록하려면:
|
||||
|
||||
| 화면 | URL | 메뉴명 예시 |
|
||||
|------|-----|-------------|
|
||||
| 결재함 | `/admin/approvalBox` | 결재함 |
|
||||
| 결재 템플릿 관리 | `/admin/approvalTemplate` | 결재 템플릿 |
|
||||
| 결재 유형 관리 | `/admin/approvalMng` | 결재 유형 (기존) |
|
||||
|
||||
---
|
||||
|
||||
## 파일 구조
|
||||
|
||||
```
|
||||
backend-node/src/
|
||||
├── controllers/
|
||||
│ ├── approvalController.ts # 결재 유형/템플릿/요청/라인 처리
|
||||
│ └── approvalProxyController.ts # 대결 위임 CRUD
|
||||
└── routes/
|
||||
└── approvalRoutes.ts # 라우트 등록
|
||||
|
||||
frontend/
|
||||
├── app/(main)/admin/
|
||||
│ ├── approvalBox/page.tsx # 결재함 (수신/상신/대결)
|
||||
│ ├── approvalTemplate/page.tsx # 템플릿 관리
|
||||
│ └── approvalMng/page.tsx # 결재 유형 관리 (기존)
|
||||
├── components/approval/
|
||||
│ └── ApprovalRequestModal.tsx # 결재 요청 모달
|
||||
└── lib/
|
||||
├── api/approval.ts # API 클라이언트
|
||||
└── registry/components/v2-approval-step/
|
||||
├── ApprovalStepComponent.tsx # 결재 단계 시각화
|
||||
└── types.ts # 확장 타입
|
||||
|
||||
db/migrations/
|
||||
├── 1051_approval_system_v2.sql # v2 스키마 확장
|
||||
└── 1052_rename_proxy_id_to_id.sql # PK 컬럼명 통일
|
||||
```
|
||||
@@ -0,0 +1,759 @@
|
||||
# WACE 시스템 문제점 분석 및 개선 계획
|
||||
|
||||
> **작성일**: 2026-03-01
|
||||
> **상태**: 분석 완료, 계획 수립
|
||||
> **목적**: 반복적으로 발생하는 시스템 문제의 근본 원인 분석 및 구조적 개선 방안
|
||||
|
||||
---
|
||||
|
||||
## 목차
|
||||
|
||||
1. [문제 요약](#1-문제-요약)
|
||||
2. [문제 1: AI(Cursor) 대화 길어질수록 정확도 저하](#2-문제-1-aicursor-대화-길어질수록-정확도-저하)
|
||||
3. [문제 2: 컴포넌트가 일관되지 않게 생성됨](#3-문제-2-컴포넌트가-일관되지-않게-생성됨)
|
||||
4. [문제 3: 코드 수정 시 다른 곳에 사이드 이펙트 발생](#4-문제-3-코드-수정-시-다른-곳에-사이드-이펙트-발생)
|
||||
5. [근본 원인 종합](#5-근본-원인-종합)
|
||||
6. [개선 계획](#6-개선-계획)
|
||||
7. [우선순위 로드맵](#7-우선순위-로드맵)
|
||||
|
||||
---
|
||||
|
||||
## 1. 문제 요약
|
||||
|
||||
| # | 증상 | 빈도 | 심각도 |
|
||||
|---|------|------|--------|
|
||||
| 1 | Cursor로 오래 작업하면 정확도 떨어짐 | 매 세션 | 중 |
|
||||
| 2 | 로우코드 컴포넌트 생성 시 오류, 비일관성 | 매 컴포넌트 | 높 |
|
||||
| 3 | 수정/신규 코드가 다른 곳에 영향 (저장 안됨, 특정 기능 깨짐) | 수시 | 높 |
|
||||
|
||||
세 문제는 독립적으로 보이지만, **하나의 구조적 원인**에서 파생된다.
|
||||
|
||||
---
|
||||
|
||||
## 2. 문제 1: AI(Cursor) 대화 길어질수록 정확도 저하
|
||||
|
||||
### 2.1. 증상
|
||||
|
||||
- 대화 초반에는 정확한 코드를 생성하다가, 30분~1시간 이상 작업하면 엉뚱한 코드 생성
|
||||
- 이전 맥락을 잊고 같은 질문을 반복하거나, 이미 수정한 부분을 되돌림
|
||||
- 관련 없는 파일을 수정하거나, 존재하지 않는 함수/변수를 참조
|
||||
|
||||
### 2.2. 원인 분석
|
||||
|
||||
AI의 컨텍스트 윈도우는 유한하다. 우리 코드베이스의 핵심 파일들이 **비정상적으로 거대**해서, AI가 한 번에 파악해야 할 정보량이 폭발한다.
|
||||
|
||||
#### 거대 파일 목록 (상위 10개)
|
||||
|
||||
| 파일 | 줄 수 | 역할 |
|
||||
|------|-------|------|
|
||||
| `frontend/lib/utils/buttonActions.ts` | **7,609줄** | 버튼 액션 전체 로직 |
|
||||
| `frontend/components/screen/ScreenDesigner.tsx` | **7,559줄** | 화면 설계기 |
|
||||
| `frontend/lib/registry/components/v2-table-list/TableListComponent.tsx` | **6,867줄** | V2 테이블 컴포넌트 |
|
||||
| `frontend/lib/registry/components/table-list/TableListComponent.tsx` | **6,829줄** | 레거시 테이블 컴포넌트 |
|
||||
| `frontend/components/screen/EditModal.tsx` | **1,648줄** | 편집 모달 |
|
||||
| `frontend/lib/registry/components/v2-button-primary/ButtonPrimaryComponent.tsx` | **1,524줄** | 버튼 컴포넌트 |
|
||||
| `frontend/components/v2/V2Repeater.tsx` | **1,442줄** | 리피터 컴포넌트 |
|
||||
| `frontend/components/screen/InteractiveScreenViewerDynamic.tsx` | **1,435줄** | 화면 뷰어 |
|
||||
| `frontend/lib/utils/improvedButtonActionExecutor.ts` | **1,063줄** | 버튼 실행기 |
|
||||
| `frontend/lib/registry/DynamicComponentRenderer.tsx` | **980줄** | 컴포넌트 렌더러 |
|
||||
|
||||
**상위 3개 파일만 합쳐도 22,035줄**이다. AI가 이 파일 하나를 읽는 것만으로도 컨텍스트의 상당 부분을 소모한다.
|
||||
|
||||
#### 타입 안전성 부재
|
||||
|
||||
```typescript
|
||||
// frontend/types/component.ts:37-39
|
||||
export interface ComponentConfig {
|
||||
[key: string]: any; // 사실상 타입 검증 없음
|
||||
}
|
||||
|
||||
// frontend/types/component.ts:56-78
|
||||
export interface ComponentRendererProps {
|
||||
component: any; // ComponentData인데 any로 선언
|
||||
// ... 중략 ...
|
||||
[key: string]: any; // 여기도 any
|
||||
}
|
||||
```
|
||||
|
||||
`any` 타입이 핵심 인터페이스에 사용되어, AI가 "이 prop에 뭘 넣어야 하는지" 추론 불가.
|
||||
사람이 봐도 모르는데 AI가 알 리가 없다.
|
||||
|
||||
#### 이벤트 이름이 문자열 상수
|
||||
|
||||
```typescript
|
||||
// 이 이벤트들이 코드 전체에 흩어져 있음
|
||||
window.dispatchEvent(new CustomEvent("refreshTable"));
|
||||
window.dispatchEvent(new CustomEvent("closeEditModal"));
|
||||
window.dispatchEvent(new CustomEvent("saveSuccessInModal"));
|
||||
window.dispatchEvent(new CustomEvent("repeaterSaveComplete"));
|
||||
window.dispatchEvent(new CustomEvent("refreshCardDisplay"));
|
||||
window.dispatchEvent(new CustomEvent("refreshTableData"));
|
||||
window.dispatchEvent(new CustomEvent("saveSuccess"));
|
||||
window.dispatchEvent(new CustomEvent("closeScreenModal"));
|
||||
```
|
||||
|
||||
문자열 기반이라 AI가 이벤트 흐름을 추적할 수 없다. 어떤 이벤트가 어디서 발생하고 어디서 수신되는지 **정적 분석이 불가능**하다.
|
||||
|
||||
### 2.3. 영향
|
||||
|
||||
- AI가 파일 하나를 읽으면 다른 파일의 맥락을 잊음
|
||||
- 함수 시그니처를 추론하지 못하고 잘못된 파라미터를 넣음
|
||||
- 이벤트 기반 로직을 이해하지 못해 부정확한 코드 생성
|
||||
|
||||
---
|
||||
|
||||
## 3. 문제 2: 컴포넌트가 일관되지 않게 생성됨
|
||||
|
||||
### 3.1. 증상
|
||||
|
||||
- 새 컴포넌트를 만들 때마다 구조가 다름
|
||||
- Config 패널의 UI 패턴이 컴포넌트마다 제각각
|
||||
- 같은 기능인데 어떤 컴포넌트는 동작하고 어떤 컴포넌트는 안 됨
|
||||
|
||||
### 3.2. 원인 분석
|
||||
|
||||
#### 컴포넌트 수량과 중복
|
||||
|
||||
현재 등록된 컴포넌트 디렉토리: **81개**
|
||||
|
||||
이 중 V2와 레거시가 병존하는 **중복 쌍**:
|
||||
|
||||
| V2 버전 | 레거시 버전 | 기능 |
|
||||
|---------|------------|------|
|
||||
| `v2-table-list` (6,867줄) | `table-list` (6,829줄) | 테이블 |
|
||||
| `v2-button-primary` (1,524줄) | `button-primary` | 버튼 |
|
||||
| `v2-card-display` | `card-display` | 카드 표시 |
|
||||
| `v2-aggregation-widget` | `aggregation-widget` | 집계 위젯 |
|
||||
| `v2-file-upload` | `file-upload` | 파일 업로드 |
|
||||
| `v2-split-panel-layout` | `split-panel-layout` | 분할 패널 |
|
||||
| `v2-section-card` | `section-card` | 섹션 카드 |
|
||||
| `v2-section-paper` | `section-paper` | 섹션 페이퍼 |
|
||||
| `v2-category-manager` | `category-manager` | 카테고리 |
|
||||
| `v2-repeater` | `repeater-field-group` | 리피터 |
|
||||
| `v2-pivot-grid` | `pivot-grid` | 피벗 그리드 |
|
||||
| `v2-rack-structure` | `rack-structure` | 랙 구조 |
|
||||
| `v2-repeat-container` | `repeat-container` | 반복 컨테이너 |
|
||||
|
||||
**13쌍이 중복** 존재. `v2-table-list`와 `table-list`는 각각 6,800줄 이상으로, 거의 같은 코드가 두 벌 있다.
|
||||
|
||||
#### 패턴은 있지만 강제되지 않음
|
||||
|
||||
컴포넌트 표준 구조:
|
||||
```
|
||||
v2-example/
|
||||
├── index.ts # createComponentDefinition()
|
||||
├── ExampleRenderer.tsx # AutoRegisteringComponentRenderer 상속
|
||||
├── ExampleComponent.tsx # 실제 UI
|
||||
├── ExampleConfigPanel.tsx # 설정 패널 (선택)
|
||||
└── types.ts # ExampleConfig extends ComponentConfig
|
||||
```
|
||||
|
||||
이 패턴을 **문서(`.cursor/rules/component-development-guide.mdc`)에서 설명**하고 있지만:
|
||||
|
||||
1. **런타임 검증 없음**: `createComponentDefinition()`이 ID 형식만 검증, 나머지는 자유
|
||||
2. **Config 타입이 `any`**: `ComponentConfig = { [key: string]: any }` → 아무 값이나 들어감
|
||||
3. **테스트 0개**: 전체 프론트엔드에 테스트 파일 **1개** (`buttonDataflowPerformance.test.ts`), 컴포넌트 테스트는 **0개**
|
||||
4. **스캐폴딩 도구 없음**: 수동으로 파일을 만들고 index.ts에 import를 추가해야 함
|
||||
|
||||
#### 컴포넌트 간 복잡도 격차
|
||||
|
||||
| 분류 | 예시 | 줄 수 | 외부 의존 | Error Boundary |
|
||||
|------|------|-------|-----------|----------------|
|
||||
| 단순 표시형 | `v2-text-display` | ~100줄 | 거의 없음 | 없음 |
|
||||
| 입력형 | `v2-input` | ~500줄 | formData, eventBus | 없음 |
|
||||
| 버튼 | `v2-button-primary` | 1,524줄 | buttonActions, apiClient, context, eventBus, modalDataStore | 있음 |
|
||||
| 테이블 | `v2-table-list` | 6,867줄 | 거의 모든 것 | 있음 |
|
||||
|
||||
100줄짜리와 6,867줄짜리가 같은 "컴포넌트"로 취급된다. AI에게 "컴포넌트 만들어"라고 하면 어떤 수준으로 만들어야 하는지 기준이 없다.
|
||||
|
||||
#### POP 컴포넌트는 완전 별도 시스템
|
||||
|
||||
```
|
||||
frontend/lib/registry/
|
||||
├── ComponentRegistry.ts # 웹 컴포넌트 레지스트리
|
||||
├── PopComponentRegistry.ts # POP 컴포넌트 레지스트리 (별도 인터페이스)
|
||||
```
|
||||
|
||||
같은 "컴포넌트"인데 등록 방식, 인터페이스, 설정 구조가 완전히 다르다.
|
||||
|
||||
### 3.3. 영향
|
||||
|
||||
- 새 컴포넌트를 만들 때 "어떤 컴포넌트를 참고해야 하는지" 불명확
|
||||
- AI가 참조하는 컴포넌트에 따라 결과물이 달라짐
|
||||
- Config 구조가 제각각이라 설정 패널 UI도 불일치
|
||||
|
||||
---
|
||||
|
||||
## 4. 문제 3: 코드 수정 시 다른 곳에 사이드 이펙트 발생
|
||||
|
||||
### 4.1. 증상
|
||||
|
||||
- 저장 로직 수정했더니 다른 화면에서 저장이 안 됨
|
||||
- 테이블 관련 코드 수정했더니 모달에서 특정 기능이 깨짐
|
||||
- 리피터 수정했더니 버튼 동작이 달라짐
|
||||
|
||||
### 4.2. 원인 분석
|
||||
|
||||
#### 원인 A: window 전역 상태 오염
|
||||
|
||||
코드베이스 전체에서 `window.__*` 패턴 사용: **8개 파일, 32회 참조**
|
||||
|
||||
| 전역 변수 | 정의 위치 | 사용 위치 | 위험도 |
|
||||
|-----------|-----------|-----------|--------|
|
||||
| `window.__v2RepeaterInstances` | `V2Repeater.tsx` (220줄) | `EditModal.tsx`, `buttonActions.ts` (4곳) | **높음** |
|
||||
| `window.__relatedButtonsTargetTables` | `RelatedDataButtonsComponent.tsx` (25줄) | `v2-table-list`, `table-list`, `buttonActions.ts` | **높음** |
|
||||
| `window.__relatedButtonsSelectedData` | `RelatedDataButtonsComponent.tsx` (51줄) | `buttonActions.ts` (3113줄) | **높음** |
|
||||
| `window.__unifiedRepeaterInstances` | `UnifiedRepeater.tsx` (110줄) | `UnifiedRepeater.tsx` | 중간 |
|
||||
| `window.__AUTH_LOG` | `authLogger.ts` | 디버깅용 | 낮음 |
|
||||
|
||||
**사이드 이펙트 시나리오 예시**:
|
||||
|
||||
```
|
||||
1. V2Repeater 마운트 → window.__v2RepeaterInstances에 등록
|
||||
2. EditModal이 저장 시 → window.__v2RepeaterInstances 체크
|
||||
3. 만약 Repeater가 언마운트 타이밍에 늦게 정리되면?
|
||||
→ EditModal은 "리피터가 있다"고 판단
|
||||
→ 리피터 저장 로직 실행
|
||||
→ 실제로는 리피터 데이터 없음
|
||||
→ 저장 실패 또는 빈 데이터 저장
|
||||
```
|
||||
|
||||
#### 원인 B: 이벤트 스파게티
|
||||
|
||||
`window.dispatchEvent(new CustomEvent(...))` 사용: **43개 파일, 총 120회 이상**
|
||||
|
||||
주요 이벤트와 발신/수신 관계:
|
||||
|
||||
```
|
||||
[refreshTable 이벤트]
|
||||
발신 (8곳):
|
||||
- buttonActions.ts (5회)
|
||||
- BomItemEditorComponent.tsx
|
||||
- SelectedItemsDetailInputComponent.tsx
|
||||
- BomTreeComponent.tsx (2회)
|
||||
- ButtonPrimaryComponent.tsx (레거시)
|
||||
- ScreenModal.tsx (2회)
|
||||
- InteractiveScreenViewerDynamic.tsx
|
||||
|
||||
수신 (5곳):
|
||||
- v2-table-list/TableListComponent.tsx
|
||||
- table-list/TableListComponent.tsx
|
||||
- SplitPanelLayoutComponent.tsx
|
||||
- InteractiveScreenViewerDynamic.tsx
|
||||
- InteractiveScreenViewer.tsx
|
||||
```
|
||||
|
||||
```
|
||||
[closeEditModal 이벤트]
|
||||
발신 (4곳):
|
||||
- buttonActions.ts (4회)
|
||||
|
||||
수신 (2곳):
|
||||
- EditModal.tsx
|
||||
- screens/[screenId]/page.tsx
|
||||
```
|
||||
|
||||
```
|
||||
[beforeFormSave 이벤트]
|
||||
수신 (6곳):
|
||||
- V2Input.tsx
|
||||
- V2Repeater.tsx
|
||||
- BomItemEditorComponent.tsx
|
||||
- SelectedItemsDetailInputComponent.tsx
|
||||
- UniversalFormModalComponent.tsx
|
||||
- V2FormContext.tsx
|
||||
```
|
||||
|
||||
**문제**: 이벤트 이름이 **문자열 상수**이고, 발신과 수신이 **타입으로 연결되지 않음**.
|
||||
`refreshTable` 이벤트를 `refreshTableData`로 오타내도 컴파일 에러 없이 런타임에서만 발견된다.
|
||||
|
||||
#### 원인 C: 이중/삼중 이벤트 시스템
|
||||
|
||||
동시에 3개의 이벤트 시스템이 공존:
|
||||
|
||||
| 시스템 | 위치 | 방식 | 타입 안전 |
|
||||
|--------|------|------|-----------|
|
||||
| `window.dispatchEvent` | 전역 | CustomEvent 문자열 | 없음 |
|
||||
| `v2EventBus` | `lib/v2-core/events/EventBus.ts` | 타입 기반 pub/sub | 있음 |
|
||||
| `LegacyEventAdapter` | `lib/v2-core/adapters/LegacyEventAdapter.ts` | 1번↔2번 브릿지 | 부분적 |
|
||||
|
||||
어떤 컴포넌트는 `window.dispatchEvent`를 쓰고, 어떤 컴포넌트는 `v2EventBus`를 쓰고, 또 어떤 컴포넌트는 둘 다 쓴다. **같은 이벤트가 두 시스템에서 동시에 발생**할 수 있어 예측 불가능한 동작이 발생한다.
|
||||
|
||||
#### 원인 D: SplitPanelContext 이름 충돌
|
||||
|
||||
같은 이름의 Context가 2개 존재:
|
||||
|
||||
| 위치 | 용도 | 제공하는 것 |
|
||||
|------|------|------------|
|
||||
| `frontend/contexts/SplitPanelContext.tsx` | 데이터 전달 | `selectedLeftData`, `transfer()`, `registerReceiver()` |
|
||||
| `frontend/lib/registry/components/split-panel-layout/SplitPanelContext.tsx` | 리사이즈/좌표 | `getAdjustedX()`, `dividerX`, `leftWidthPercent` |
|
||||
|
||||
import 경로에 따라 **완전히 다른 Context**를 가져온다. AI가 자동완성으로 잘못된 Context를 import하면 런타임에 `undefined` 에러가 발생한다.
|
||||
|
||||
#### 원인 E: buttonActions.ts - 7,609줄의 신(God) 파일
|
||||
|
||||
이 파일 하나가 다음 기능을 전부 담당:
|
||||
|
||||
- 저장 (INSERT/UPDATE/DELETE)
|
||||
- 모달 열기/닫기
|
||||
- 리피터 데이터 수집
|
||||
- 테이블 새로고침
|
||||
- 파일 업로드
|
||||
- 외부 API 호출
|
||||
- 화면 전환
|
||||
- 데이터 검증
|
||||
- 이벤트 발송 (33회)
|
||||
- window 전역 상태 읽기 (5회)
|
||||
|
||||
**이 파일의 한 줄을 수정하면, 위의 모든 기능이 영향을 받을 수 있다.**
|
||||
|
||||
#### 원인 F: 레거시-V2 코드 동시 존재
|
||||
|
||||
```
|
||||
v2-table-list/TableListComponent.tsx (6,867줄)
|
||||
table-list/TableListComponent.tsx (6,829줄)
|
||||
```
|
||||
|
||||
거의 같은 코드가 두 벌. 한쪽을 수정하면 다른 쪽은 수정 안 되어 동작이 달라진다.
|
||||
또한 두 컴포넌트가 **같은 전역 이벤트를 수신**하므로, 한 화면에 둘 다 있으면 이중으로 반응할 수 있다.
|
||||
|
||||
#### 원인 G: Error Boundary 미적용
|
||||
|
||||
| 컴포넌트 | Error Boundary |
|
||||
|----------|----------------|
|
||||
| `v2-button-primary` | 있음 |
|
||||
| `v2-table-list` | 있음 |
|
||||
| `v2-repeater` | 있음 |
|
||||
| `v2-input` | **없음** |
|
||||
| `v2-select` | **없음** |
|
||||
| `v2-card-display` | **없음** |
|
||||
| `v2-text-display` | **없음** |
|
||||
| 기타 대부분 | **없음** |
|
||||
|
||||
Error Boundary가 없는 컴포넌트에서 에러가 발생하면, **상위 컴포넌트까지 전파**되어 화면 전체가 깨진다.
|
||||
|
||||
### 4.3. 사이드 이펙트 발생 위험 지도
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ buttonActions.ts │
|
||||
│ (7,609줄) │
|
||||
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
|
||||
│ │ 저장 로직 │ │ 모달 로직 │ │ 이벤트 │ │
|
||||
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
|
||||
└───────┼──────────────┼─────────────┼─────────────────┘
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
┌──────────────┐ ┌──────────┐ ┌─────────────────┐
|
||||
│ window.__v2 │ │EditModal │ │ CustomEvent │
|
||||
│ RepeaterInst │ │(1,648줄) │ │ "refreshTable" │
|
||||
│ ances │ │ │ │ "closeEditModal" │
|
||||
└──────┬───────┘ └────┬─────┘ │ "saveSuccess" │
|
||||
│ │ └───────┬─────────┘
|
||||
▼ │ │
|
||||
┌──────────────┐ │ ┌──────▼───────┐
|
||||
│ V2Repeater │◄─────┘ │ TableList │
|
||||
│ (1,442줄) │ │ (6,867줄) │
|
||||
└──────────────┘ │ + 레거시 │
|
||||
│ (6,829줄) │
|
||||
└──────────────┘
|
||||
```
|
||||
|
||||
**위 그래프에서 어디를 수정하든 화살표를 따라 다른 곳에 영향이 전파된다.**
|
||||
|
||||
---
|
||||
|
||||
## 5. 근본 원인 종합
|
||||
|
||||
세 가지 문제의 근본 원인은 하나다: **경계(Boundary)가 없는 아키텍처**
|
||||
|
||||
| 근본 원인 | 문제 1 영향 | 문제 2 영향 | 문제 3 영향 |
|
||||
|-----------|-------------|-------------|-------------|
|
||||
| 거대 파일 (God File) | AI 컨텍스트 소모 | 참조할 기준 불명확 | 수정 영향 범위 광범위 |
|
||||
| `any` 타입 남발 | AI 타입 추론 불가 | Config 검증 없음 | 런타임 에러 |
|
||||
| 문자열 이벤트 | AI 이벤트 흐름 추적 불가 | 이벤트 패턴 불일치 | 이벤트 누락/오타 |
|
||||
| window 전역 상태 | AI 상태 추적 불가 | 컴포넌트 간 의존 증가 | 상태 오염 |
|
||||
| 테스트 부재 (0개) | 변경 검증 불가 | 컴포넌트 계약 불명 | 사이드 이펙트 감지 불가 |
|
||||
| 레거시-V2 중복 (13쌍) | AI 혼동 | 어느 쪽을 기준으로? | 한쪽만 수정 시 불일치 |
|
||||
|
||||
---
|
||||
|
||||
## 6. 개선 계획
|
||||
|
||||
### Phase 1: 즉시 효과 (1~2주) - 안전장치 설치
|
||||
|
||||
#### 1-1. 이벤트 이름 상수화
|
||||
|
||||
**현재**:
|
||||
```typescript
|
||||
window.dispatchEvent(new CustomEvent("refreshTable"));
|
||||
```
|
||||
|
||||
**개선**:
|
||||
```typescript
|
||||
// frontend/lib/constants/events.ts
|
||||
export const EVENTS = {
|
||||
REFRESH_TABLE: "refreshTable",
|
||||
CLOSE_EDIT_MODAL: "closeEditModal",
|
||||
SAVE_SUCCESS: "saveSuccess",
|
||||
SAVE_SUCCESS_IN_MODAL: "saveSuccessInModal",
|
||||
REPEATER_SAVE_COMPLETE: "repeaterSaveComplete",
|
||||
REFRESH_CARD_DISPLAY: "refreshCardDisplay",
|
||||
REFRESH_TABLE_DATA: "refreshTableData",
|
||||
CLOSE_SCREEN_MODAL: "closeScreenModal",
|
||||
BEFORE_FORM_SAVE: "beforeFormSave",
|
||||
} as const;
|
||||
|
||||
// 사용
|
||||
window.dispatchEvent(new CustomEvent(EVENTS.REFRESH_TABLE));
|
||||
```
|
||||
|
||||
**효과**: 오타 방지, AI가 이벤트 흐름 추적 가능, IDE 자동완성 지원
|
||||
**위험도**: 낮음 (기능 변경 없음, 리팩토링만)
|
||||
**소요 예상**: 2~3시간
|
||||
|
||||
#### 1-2. window 전역 변수 타입 선언
|
||||
|
||||
**현재**: `window.__v2RepeaterInstances`를 사용하지만 타입 선언 없음
|
||||
|
||||
**개선**:
|
||||
```typescript
|
||||
// frontend/types/global.d.ts
|
||||
declare global {
|
||||
interface Window {
|
||||
__v2RepeaterInstances?: Set<string>;
|
||||
__unifiedRepeaterInstances?: Set<string>;
|
||||
__relatedButtonsTargetTables?: Set<string>;
|
||||
__relatedButtonsSelectedData?: {
|
||||
tableName: string;
|
||||
selectedRows: any[];
|
||||
};
|
||||
__AUTH_LOG?: { show: () => void };
|
||||
__COMPONENT_REGISTRY__?: Map<string, any>;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**효과**: 타입 안전성 확보, AI가 전역 상태 구조 이해 가능
|
||||
**위험도**: 낮음 (타입 선언만, 런타임 변경 없음)
|
||||
**소요 예상**: 1시간
|
||||
|
||||
#### 1-3. ComponentConfig에 제네릭 타입 적용
|
||||
|
||||
**현재**:
|
||||
```typescript
|
||||
export interface ComponentConfig {
|
||||
[key: string]: any;
|
||||
}
|
||||
```
|
||||
|
||||
**개선**:
|
||||
```typescript
|
||||
export interface ComponentConfig {
|
||||
[key: string]: unknown; // any → unknown으로 변경하여 타입 체크 강제
|
||||
}
|
||||
|
||||
// 각 컴포넌트에서
|
||||
export interface ButtonPrimaryConfig extends ComponentConfig {
|
||||
text: string; // 구체적 타입
|
||||
action: ButtonAction; // 구체적 타입
|
||||
variant?: "default" | "destructive" | "outline";
|
||||
}
|
||||
```
|
||||
|
||||
**효과**: 잘못된 config 값 사전 차단
|
||||
**위험도**: 중간 (기존 `any` 사용처에서 타입 에러 발생 가능, 점진적 적용 필요)
|
||||
**소요 예상**: 3~5일 (점진적)
|
||||
|
||||
---
|
||||
|
||||
### Phase 2: 구조 개선 (2~4주) - 핵심 분리
|
||||
|
||||
#### 2-1. buttonActions.ts 분할
|
||||
|
||||
**현재**: 7,609줄, 1개 파일
|
||||
|
||||
**개선 목표**: 도메인별 분리
|
||||
|
||||
```
|
||||
frontend/lib/actions/
|
||||
├── index.ts # re-export
|
||||
├── types.ts # 공통 타입
|
||||
├── saveActions.ts # INSERT/UPDATE 저장 로직
|
||||
├── deleteActions.ts # DELETE 로직
|
||||
├── modalActions.ts # 모달 열기/닫기
|
||||
├── tableActions.ts # 테이블 새로고침, 데이터 조작
|
||||
├── repeaterActions.ts # 리피터 데이터 수집/저장
|
||||
├── fileActions.ts # 파일 업로드/다운로드
|
||||
├── navigationActions.ts # 화면 전환
|
||||
├── validationActions.ts # 데이터 검증
|
||||
└── externalActions.ts # 외부 API 호출
|
||||
```
|
||||
|
||||
**효과**:
|
||||
- 저장 로직 수정 시 `saveActions.ts`만 영향
|
||||
- AI가 관련 파일만 읽으면 됨 (7,600줄 → 평균 500줄)
|
||||
- import 관계로 의존성 명확화
|
||||
|
||||
**위험도**: 높음 (가장 많이 사용되는 파일, 신중한 분리 필요)
|
||||
**소요 예상**: 1~2주
|
||||
|
||||
#### 2-2. 이벤트 시스템 통일
|
||||
|
||||
**현재**: 3개 시스템 공존 (window CustomEvent, v2EventBus, LegacyEventAdapter)
|
||||
|
||||
**개선**:
|
||||
```typescript
|
||||
// v2EventBus로 통일, 타입 안전한 이벤트 정의
|
||||
interface EventMap {
|
||||
"table:refresh": { tableId?: string };
|
||||
"modal:close": { modalId: string };
|
||||
"form:save": { formData: Record<string, any> };
|
||||
"form:saveComplete": { success: boolean; message?: string };
|
||||
"repeater:saveComplete": { repeaterId: string };
|
||||
}
|
||||
|
||||
// 사용
|
||||
v2EventBus.emit("table:refresh", { tableId: "order_table" });
|
||||
v2EventBus.on("table:refresh", (data) => { /* data.tableId 타입 안전 */ });
|
||||
```
|
||||
|
||||
**마이그레이션 전략**:
|
||||
1. `v2EventBus`에 `EventMap` 타입 추가
|
||||
2. 새 코드는 반드시 `v2EventBus` 사용
|
||||
3. 기존 `window.dispatchEvent` → `v2EventBus`로 점진적 교체
|
||||
4. `LegacyEventAdapter`에서 양방향 브릿지 유지 (과도기)
|
||||
5. 모든 교체 완료 후 `LegacyEventAdapter` 제거
|
||||
|
||||
**효과**: 이벤트 흐름 추적 가능, 타입 안전, 디버깅 용이
|
||||
**위험도**: 중간 (과도기 브릿지로 안전하게 전환)
|
||||
**소요 예상**: 2~3주
|
||||
|
||||
#### 2-3. window 전역 상태 → Zustand 스토어 전환
|
||||
|
||||
**현재**:
|
||||
```typescript
|
||||
window.__v2RepeaterInstances = new Set();
|
||||
window.__relatedButtonsSelectedData = { tableName, selectedRows };
|
||||
```
|
||||
|
||||
**개선**:
|
||||
```typescript
|
||||
// frontend/lib/stores/componentInstanceStore.ts
|
||||
import { create } from "zustand";
|
||||
|
||||
interface ComponentInstanceState {
|
||||
repeaterInstances: Set<string>;
|
||||
relatedButtonsTargetTables: Set<string>;
|
||||
relatedButtonsSelectedData: {
|
||||
tableName: string;
|
||||
selectedRows: any[];
|
||||
} | null;
|
||||
|
||||
registerRepeater: (key: string) => void;
|
||||
unregisterRepeater: (key: string) => void;
|
||||
setRelatedData: (data: { tableName: string; selectedRows: any[] }) => void;
|
||||
clearRelatedData: () => void;
|
||||
}
|
||||
|
||||
export const useComponentInstanceStore = create<ComponentInstanceState>((set) => ({
|
||||
repeaterInstances: new Set(),
|
||||
relatedButtonsTargetTables: new Set(),
|
||||
relatedButtonsSelectedData: null,
|
||||
|
||||
registerRepeater: (key) =>
|
||||
set((state) => {
|
||||
const next = new Set(state.repeaterInstances);
|
||||
next.add(key);
|
||||
return { repeaterInstances: next };
|
||||
}),
|
||||
unregisterRepeater: (key) =>
|
||||
set((state) => {
|
||||
const next = new Set(state.repeaterInstances);
|
||||
next.delete(key);
|
||||
return { repeaterInstances: next };
|
||||
}),
|
||||
setRelatedData: (data) => set({ relatedButtonsSelectedData: data }),
|
||||
clearRelatedData: () => set({ relatedButtonsSelectedData: null }),
|
||||
}));
|
||||
```
|
||||
|
||||
**효과**:
|
||||
- 상태 변경 추적 가능 (Zustand devtools)
|
||||
- 컴포넌트 리렌더링 최적화 (selector 사용)
|
||||
- window 오염 제거
|
||||
|
||||
**위험도**: 중간
|
||||
**소요 예상**: 1주
|
||||
|
||||
---
|
||||
|
||||
### Phase 3: 품질 강화 (4~8주) - 예방 체계
|
||||
|
||||
#### 3-1. 레거시 컴포넌트 제거
|
||||
|
||||
**목표**: V2-레거시 중복 13쌍 → V2만 유지
|
||||
|
||||
**전략**:
|
||||
1. 각 중복 쌍에서 레거시 사용처 검색
|
||||
2. 사용처가 없는 레거시 컴포넌트 즉시 제거
|
||||
3. 사용처가 있는 경우 V2로 교체 후 제거
|
||||
4. `components/index.ts`에서 import 제거
|
||||
|
||||
**효과**: 코드베이스 ~15,000줄 감소, AI 혼동 제거
|
||||
**소요 예상**: 2~3주
|
||||
|
||||
#### 3-2. 컴포넌트 스캐폴딩 CLI
|
||||
|
||||
**목표**: `npx create-v2-component my-component` 실행 시 표준 구조 자동 생성
|
||||
|
||||
```bash
|
||||
$ npx create-v2-component my-widget --category data
|
||||
|
||||
생성 완료:
|
||||
frontend/lib/registry/components/v2-my-widget/
|
||||
├── index.ts # 자동 생성
|
||||
├── MyWidgetRenderer.tsx # 자동 생성
|
||||
├── MyWidgetComponent.tsx # 템플릿
|
||||
├── MyWidgetConfigPanel.tsx # 템플릿
|
||||
└── types.ts # Config 인터페이스 템플릿
|
||||
|
||||
components/index.ts에 import 자동 추가 완료
|
||||
```
|
||||
|
||||
**효과**: 컴포넌트 구조 100% 일관성 보장
|
||||
**소요 예상**: 3~5일
|
||||
|
||||
#### 3-3. 핵심 컴포넌트 통합 테스트
|
||||
|
||||
**목표**: 사이드 이펙트 감지용 테스트 작성
|
||||
|
||||
```typescript
|
||||
// __tests__/integration/save-flow.test.ts
|
||||
describe("저장 플로우", () => {
|
||||
it("버튼 저장 → refreshTable 이벤트 발생", async () => {
|
||||
const listener = vi.fn();
|
||||
v2EventBus.on("table:refresh", listener);
|
||||
|
||||
await executeSaveAction({ tableName: "test_table", data: mockData });
|
||||
|
||||
expect(listener).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("리피터가 있을 때 저장 → 리피터 데이터도 포함", async () => {
|
||||
useComponentInstanceStore.getState().registerRepeater("detail_table");
|
||||
|
||||
const result = await executeSaveAction({ tableName: "master_table", data: mockData });
|
||||
|
||||
expect(result.repeaterDataCollected).toBe(true);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
**대상**: 저장/삭제/모달/리피터 흐름 (가장 빈번하게 깨지는 부분)
|
||||
**효과**: 코드 수정 후 즉시 사이드 이펙트 감지
|
||||
**소요 예상**: 2~3주
|
||||
|
||||
#### 3-4. SplitPanelContext 통합
|
||||
|
||||
**목표**: 이름이 같은 2개의 Context → 1개로 통합 또는 명확히 분리
|
||||
|
||||
**방안 A - 통합**:
|
||||
```typescript
|
||||
// frontend/contexts/SplitPanelContext.tsx에 통합
|
||||
interface SplitPanelContextValue {
|
||||
// 데이터 전달 (기존 contexts/ 버전)
|
||||
selectedLeftData: any;
|
||||
transfer: (data: any) => void;
|
||||
registerReceiver: (handler: (data: any) => void) => void;
|
||||
// 리사이즈 (기존 components/ 버전)
|
||||
getAdjustedX: (x: number) => number;
|
||||
dividerX: number;
|
||||
leftWidthPercent: number;
|
||||
}
|
||||
```
|
||||
|
||||
**방안 B - 명확 분리**:
|
||||
```typescript
|
||||
// SplitPanelDataContext.tsx → 데이터 전달용
|
||||
// SplitPanelResizeContext.tsx → 리사이즈용
|
||||
```
|
||||
|
||||
**효과**: import 혼동 제거
|
||||
**소요 예상**: 2~3일
|
||||
|
||||
---
|
||||
|
||||
### Phase 4: 장기 개선 (8주+) - 아키텍처 전환
|
||||
|
||||
#### 4-1. 거대 컴포넌트 분할
|
||||
|
||||
| 대상 파일 | 현재 줄 수 | 분할 목표 |
|
||||
|-----------|-----------|-----------|
|
||||
| `v2-table-list/TableListComponent.tsx` | 6,867줄 | 훅 분리, 렌더링 분리 → 각 1,000줄 이하 |
|
||||
| `ScreenDesigner.tsx` | 7,559줄 | 패널별 분리 → 각 1,500줄 이하 |
|
||||
| `EditModal.tsx` | 1,648줄 | 저장/폼/UI 분리 → 각 500줄 이하 |
|
||||
| `ButtonPrimaryComponent.tsx` | 1,524줄 | 액션 실행 분리 → 각 500줄 이하 |
|
||||
|
||||
#### 4-2. Config 스키마 검증 (Zod)
|
||||
|
||||
```typescript
|
||||
// v2-button-primary/types.ts
|
||||
import { z } from "zod";
|
||||
|
||||
export const ButtonPrimaryConfigSchema = z.object({
|
||||
text: z.string().default("버튼"),
|
||||
variant: z.enum(["default", "destructive", "outline", "secondary", "ghost"]).default("default"),
|
||||
action: z.object({
|
||||
type: z.enum(["save", "delete", "navigate", "custom"]),
|
||||
targetTable: z.string().optional(),
|
||||
// ...
|
||||
}),
|
||||
});
|
||||
|
||||
export type ButtonPrimaryConfig = z.infer<typeof ButtonPrimaryConfigSchema>;
|
||||
```
|
||||
|
||||
`createComponentDefinition()`에서 스키마 검증을 강제하여 잘못된 config가 등록 시점에 차단되도록 한다.
|
||||
|
||||
---
|
||||
|
||||
## 7. 우선순위 로드맵
|
||||
|
||||
### 즉시 (이번 주)
|
||||
|
||||
- [ ] **1-1**: 이벤트 이름 상수 파일 생성 (`frontend/lib/constants/events.ts`)
|
||||
- [ ] **1-2**: window 전역 변수 타입 선언 (`frontend/types/global.d.ts`)
|
||||
|
||||
### 단기 (1~2주)
|
||||
|
||||
- [ ] **2-3**: window 전역 상태 → Zustand 스토어 전환
|
||||
- [ ] **1-3**: ComponentConfig `any` → `unknown` 점진적 적용
|
||||
|
||||
### 중기 (2~4주)
|
||||
|
||||
- [ ] **2-1**: buttonActions.ts 분할 (7,609줄 → 도메인별)
|
||||
- [ ] **2-2**: 이벤트 시스템 통일 (v2EventBus 기반)
|
||||
- [ ] **3-4**: SplitPanelContext 통합/분리
|
||||
|
||||
### 장기 (4~8주)
|
||||
|
||||
- [ ] **3-1**: 레거시 컴포넌트 13쌍 제거
|
||||
- [ ] **3-2**: 컴포넌트 스캐폴딩 CLI
|
||||
- [ ] **3-3**: 핵심 플로우 통합 테스트
|
||||
- [ ] **4-1**: 거대 컴포넌트 분할
|
||||
- [ ] **4-2**: Config 스키마 Zod 검증
|
||||
|
||||
---
|
||||
|
||||
## 부록: 수치 요약
|
||||
|
||||
| 지표 | 현재 | 목표 |
|
||||
|------|------|------|
|
||||
| 최대 파일 크기 | 7,609줄 | 1,500줄 이하 |
|
||||
| 컴포넌트 수 | 81개 (13쌍 중복) | ~55개 (중복 제거) |
|
||||
| window 전역 변수 | 5개 | 0개 |
|
||||
| 이벤트 시스템 | 3개 공존 | 1개 (v2EventBus) |
|
||||
| 테스트 파일 | 1개 | 핵심 플로우 최소 10개 |
|
||||
| `any` 타입 사용 (핵심 인터페이스) | 3곳 | 0곳 |
|
||||
| SplitPanelContext 중복 | 2개 | 1개 (또는 명확 분리) |
|
||||
@@ -0,0 +1,361 @@
|
||||
# Agent Pipeline 한계점 분석
|
||||
|
||||
> 결재 시스템 같은 대규모 크로스도메인 프로젝트에서 현재 파이프라인이 왜 제대로 동작할 수 없는가
|
||||
|
||||
---
|
||||
|
||||
## 1. 에이전트 컨텍스트 격리 문제
|
||||
|
||||
### 현상
|
||||
`executor.ts`의 `spawnAgent()`는 매번 새로운 Cursor Agent CLI 프로세스를 생성한다. 각 에이전트는 `systemPrompt + taskDescription + fileContext`만 받고, 이전 대화/결정/아키텍처 논의는 전혀 알지 못한다.
|
||||
|
||||
```typescript
|
||||
// executor.ts:64-118
|
||||
function spawnAgent(agentType, prompt, model, workspacePath, timeoutMs) {
|
||||
const child = spawn(agentPath, ['--model', model, '--print', '--trust'], {
|
||||
cwd: workspacePath,
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
});
|
||||
child.stdin.write(prompt); // 이게 에이전트가 받는 전부
|
||||
child.stdin.end();
|
||||
}
|
||||
```
|
||||
|
||||
### 문제 본질
|
||||
- 에이전트는 **"왜 이렇게 만들어야 하는지"** 모른다. 단지 task description에 적힌 대로 만든다
|
||||
- 결재 시스템의 **설계 의도** (한국 기업 결재 문화, 자기결재/상신결재/합의결재/대결/후결)는 task description에 다 담을 수 없다
|
||||
- PM과 사용자 사이에 오간 **아키텍처 논의** (이벤트 훅 시스템, 제어관리 연동, 엔티티 조인으로 결재 상태 표시) 같은 결정 사항이 전달되지 않는다
|
||||
|
||||
### 결재 시스템에서의 구체적 영향
|
||||
- "ApprovalRequestModal에 결재 유형 선택을 추가해라"라고 지시하면, 에이전트는 기존 모달 코드를 읽겠지만, **왜 그 UI가 그렇게 생겼는지, 다른 패널(TableListConfigPanel)의 Combobox 패턴을 왜 따라야 하는지** 모른다
|
||||
- 실제로 이 대화에서 Combobox UI가 4번 수정됐다. 매번 "다른 패널 참고해서 만들라"고 해도 패턴을 정확히 못 따라했다
|
||||
|
||||
---
|
||||
|
||||
## 2. 파일 컨텍스트 3000자 절삭
|
||||
|
||||
### 현상
|
||||
```typescript
|
||||
// executor.ts:124-138
|
||||
async function readFileContexts(files, workspacePath) {
|
||||
for (const file of files) {
|
||||
const content = await readFile(fullPath, 'utf-8');
|
||||
contents.push(`--- ${file} ---\n${content.substring(0, 3000)}`); // 3000자 잘림
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 문제 본질
|
||||
주요 파일들의 실제 크기:
|
||||
- `approvalController.ts`: ~800줄 (3000자로는 약 100줄, 12.5%만 보인다)
|
||||
- `improvedButtonActionExecutor.ts`: ~1500줄
|
||||
- `ButtonConfigPanel.tsx`: ~600줄
|
||||
- `ApprovalStepConfigPanel.tsx`: ~300줄
|
||||
|
||||
에이전트가 수정해야 할 파일의 **전체 구조를 이해할 수 없다**. 앞부분만 보고 import 구문이나 초기 코드만 파악하고, 실제 수정 지점에 도달하지 못한다.
|
||||
|
||||
### 결재 시스템에서의 구체적 영향
|
||||
- `approvalController.ts`를 수정하려면 기존 함수 구조, DB 쿼리 패턴, 에러 처리 방식, 멀티테넌시 적용 패턴을 전부 알아야 한다. 3000자로는 불가능
|
||||
- `improvedButtonActionExecutor.ts`의 제어관리 연동 패턴을 이해하려면 파일 전체를 봐야 한다
|
||||
- V2 컴포넌트 표준을 따르려면 기존 컴포넌트(`v2-table-list/` 등)의 전체 구조를 참고해야 한다
|
||||
|
||||
---
|
||||
|
||||
## 3. 에이전트 간 실시간 소통 부재
|
||||
|
||||
### 현상
|
||||
병렬 실행 시 에이전트들은 **서로의 작업 결과를 실시간으로 공유하지 못한다**:
|
||||
|
||||
```typescript
|
||||
// executor.ts:442-454
|
||||
if (state.config.parallel) {
|
||||
const promises = readyTasks.map(async (task, index) => {
|
||||
if (index > 0) await sleep(index * STAGGER_DELAY); // 500ms 딜레이뿐
|
||||
return executeAndTrack(task);
|
||||
});
|
||||
await Promise.all(promises); // 완료까지 기다린 후 PM이 리뷰
|
||||
}
|
||||
```
|
||||
|
||||
PM 에이전트가 라운드 후에 리뷰하지만, 이것도 **round-N.md의 텍스트 기반 리뷰**일 뿐이다.
|
||||
|
||||
### 문제 본질
|
||||
- DB 에이전트가 스키마를 변경하면, Backend 에이전트가 그 결과를 **같은 라운드에서 즉시 반영할 수 없다**
|
||||
- Frontend 에이전트가 "이 API 응답 구조 좀 바꿔줘"라고 Backend에 요청할 수 없다
|
||||
- 협업 모드(`CollabMessage`)가 존재하지만, 이것도 **라운드 단위의 비동기 메시지**이지 실시간 대화가 아니다
|
||||
|
||||
### 결재 시스템에서의 구체적 영향
|
||||
- DB가 `approval_proxy_settings` 테이블을 만들고, Backend가 대결 API를 만들고, Frontend가 대결 설정 UI를 만드는 과정이 **최소 3라운드**가 필요하다 (각 의존성 해소를 위해)
|
||||
- 실제로는 Backend가 DB 스키마를 보고 쿼리를 짜는 과정에서 "이 컬럼 타입이 좀 다른 것 같은데"라는 이슈가 생기면, 즉시 수정 불가하고 다음 라운드로 넘어간다
|
||||
- 라운드당 에이전트 호출 1~3분 + PM 리뷰 1~2분 = **라운드당 최소 3~5분**. 8개 phase를 3라운드씩 = **최소 72~120분 (1~2시간)**
|
||||
|
||||
---
|
||||
|
||||
## 4. 시스템 프롬프트의 한계 (프로젝트 특수 패턴 부재)
|
||||
|
||||
### 현상
|
||||
`prompts.ts`의 시스템 프롬프트는 **범용적**이다:
|
||||
|
||||
```typescript
|
||||
// prompts.ts:75-118
|
||||
export const BACKEND_PROMPT = `
|
||||
# Role
|
||||
You are a Backend specialist for ERP-node project.
|
||||
Stack: Node.js + Express + TypeScript + PostgreSQL Raw Query.
|
||||
// ... 멀티테넌시, 기본 코드 패턴만 포함
|
||||
`;
|
||||
```
|
||||
|
||||
### 프로젝트 특수 패턴 중 프롬프트에 없는 것들
|
||||
|
||||
| 필수 패턴 | 프롬프트 포함 여부 | 영향 |
|
||||
|-----------|:------------------:|------|
|
||||
| V2 컴포넌트 레지스트리 (`createComponentDefinition`, `AutoRegisteringComponentRenderer`) | 프론트엔드 프롬프트에 기본 구조만 | 컴포넌트 등록 방식 오류 가능 |
|
||||
| ConfigPanelBuilder / ConfigSection | 언급만 | 직접 JSX로 패널 만드는 실수 반복 |
|
||||
| Combobox UI 패턴 (Popover + Command) | 없음 | 실제로 4번 재수정 필요했음 |
|
||||
| 엔티티 조인 시스템 | 없음 | 결재 상태를 대상 테이블에 표시하는 핵심 기능 구현 불가 |
|
||||
| 제어관리(Node Flow) 연동 | 없음 | 결재 후 자동 액션 트리거 구현 불가 |
|
||||
| ButtonActionExecutor 패턴 | 없음 | 결재 버튼 액션 구현 시 기존 패턴 미준수 |
|
||||
| apiClient 사용법 (frontend/lib/api/) | 간략한 언급 | fetch 직접 사용 가능성 |
|
||||
| CustomEvent 기반 모달 오픈 | 없음 | approval-modal 열기 방식 이해 불가 |
|
||||
| 화면 디자이너 컨텍스트 | 없음 | screenTableName 같은 설계 시 컨텍스트 활용 불가 |
|
||||
|
||||
### 결재 시스템에서의 구체적 영향
|
||||
- **이벤트 훅 시스템**을 만들려면 기존 `NodeFlowExecutionService`의 실행 패턴, 액션 타입 enum, 입력/출력 구조를 알아야 하는데, 프롬프트에 전혀 없다
|
||||
- **엔티티 조인으로 결재 상태 표시**하려면 기존 엔티티 조인 시스템이 어떻게 작동하는지(reverse lookup, join config) 알아야 하는데, 에이전트가 이 시스템 자체를 모른다
|
||||
|
||||
---
|
||||
|
||||
## 5. 단일 패스 실행 + 재시도의 비효율
|
||||
|
||||
### 현상
|
||||
```typescript
|
||||
// executor.ts:240-288
|
||||
async function executeTaskWithRetry(task, state) {
|
||||
while (task.attempts < task.maxRetries) {
|
||||
const result = await executeTaskOnce(task, state, retryContext);
|
||||
task.attempts++;
|
||||
if (result.success) break;
|
||||
// 검증 실패 → retryContext에 에러 메시지만 전달
|
||||
retryContext = failResult.retryContext || `이전 시도 실패: ${result.agentOutput.substring(0, 500)}`;
|
||||
await sleep(2000);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 문제 본질
|
||||
- 재시도 시 에이전트가 받는 건 **이전 에러 메시지 500자**뿐이다
|
||||
- "Combobox 패턴 대신 Select 박스를 썼다" 같은 **UI/UX 품질 문제**는 L1~L6 검증으로 잡을 수 없다 (빌드는 통과하니까)
|
||||
- 사용자의 실시간 피드백("이거 다른 패널이랑 UI가 다른데?")을 반영할 수 없다
|
||||
|
||||
### 검증 피라미드(L1~L6)가 못 잡는 것들
|
||||
|
||||
| 검증 레벨 | 잡을 수 있는 것 | 못 잡는 것 |
|
||||
|-----------|----------------|-----------|
|
||||
| L1 (TS 빌드) | 타입 에러, import 오류 | 로직 오류, 패턴 미준수 |
|
||||
| L2 (앱 빌드) | Next.js 빌드 에러 | 런타임 에러 |
|
||||
| L3 (API 호출) | 엔드포인트 존재 여부, 기본 응답 | 복잡한 비즈니스 로직 (다단계 결재 플로우) |
|
||||
| L4 (DB 검증) | 테이블 존재, 기본 CRUD | 결재 상태 전이 로직, 병렬 결재 집계 |
|
||||
| L5 (브라우저 E2E) | 화면 렌더링, 기본 클릭 | 결재 모달 Combobox UX, 대결 설정 UI 일관성 |
|
||||
| L6 (커스텀) | 명시적 조건 | 비명시적 품질 요구사항 |
|
||||
|
||||
### 결재 시스템에서의 구체적 영향
|
||||
- "자기결재 시 즉시 approved로 처리"가 올바르게 동작하는지 L3/L4로 검증 가능하지만, **"자기결재 선택 시 결재자 선택 UI가 숨겨지고 즉시 처리된다"는 UX**는 L5 자연어로는 불충분
|
||||
- "합의결재(병렬)에서 3명 중 2명 승인 + 1명 반려 시 전체 반려" 같은 **엣지 케이스 비즈니스 로직**은 자동 검증이 어렵다
|
||||
- 결재 완료 후 이벤트 훅 → Node Flow 실행 → 이메일 발송 같은 **체이닝된 비동기 로직**은 E2E로 검증 불가
|
||||
|
||||
---
|
||||
|
||||
## 6. 태스크 분할의 구조적 한계
|
||||
|
||||
### 현상: 파이프라인이 잘 되는 경우
|
||||
```
|
||||
[DB 테이블 생성] → [Backend CRUD API] → [Frontend 화면] → [UI 개선]
|
||||
```
|
||||
각 태스크가 **독립적**이고, 새 파일을 만들고, 의존성이 단방향이다.
|
||||
|
||||
### 현상: 파이프라인이 안 되는 경우 (결재 시스템)
|
||||
```
|
||||
[DB 스키마 변경]
|
||||
↓ ↘
|
||||
[Controller 수정] [새 API 추가] ← 기존 코드 500줄 이해 필요
|
||||
↓ ↓ ↑
|
||||
[모달 수정] [새 화면] ← 기존 UI 패턴 준수 필요 + 엔티티 조인 시스템 이해
|
||||
↓
|
||||
[V2 컴포넌트 수정] ← 레지스트리 시스템 + ConfigPanelBuilder 패턴 이해
|
||||
↓
|
||||
[이벤트 훅 시스템] ← NodeFlowExecutionService 전체 이해 + 새 시스템 설계
|
||||
↓
|
||||
[엔티티 조인 등록] ← 기존 엔티티 조인 시스템 전체 이해
|
||||
```
|
||||
|
||||
### 문제 본질
|
||||
- **기존 파일 수정**이 대부분이다. 새 파일 생성이 아니라 기존 코드에 기능을 끼워넣어야 한다
|
||||
- **패턴 준수**가 필수다. "돌아가기만 하면" 안 되고, 기존 시스템과 **일관된 방식**으로 구현해야 한다
|
||||
- **설계 결정**이 코드 작성보다 중요하다. "이벤트 훅을 어떻게 설계할까?"는 에이전트가 task description만 보고 결정할 수 없다
|
||||
|
||||
---
|
||||
|
||||
## 7. PM 에이전트의 역할 한계
|
||||
|
||||
### 현상
|
||||
```typescript
|
||||
// pm-agent.ts:21-70
|
||||
const PM_SYSTEM_PROMPT = `
|
||||
# 판단 기준
|
||||
- 빌드만 통과하면 "complete" 아니다 -- 기능이 실제로 동작해야 "complete"
|
||||
- 같은 에러 2회 반복 -> instruction에 구체적 해결책 제시
|
||||
- 같은 에러 3회 반복 -> "fail" 판정
|
||||
`;
|
||||
```
|
||||
|
||||
PM은 `round-N.md`(에이전트 응답 + git diff + 테스트 결과)와 `progress.md`만 보고 판단한다.
|
||||
|
||||
### PM이 할 수 없는 것
|
||||
|
||||
| 역할 | PM 가능 여부 | 이유 |
|
||||
|------|:----------:|------|
|
||||
| 빌드 실패 원인 파악 | 가능 | 에러 로그가 round-N.md에 있음 |
|
||||
| 비즈니스 로직 검증 | 불가 | 실제 코드를 읽지 않고 git diff만 봄 |
|
||||
| UI/UX 품질 판단 | 불가 | 스크린샷 없음, 렌더링 결과 못 봄 |
|
||||
| 아키텍처 일관성 검증 | 불가 | 전체 시스템 구조를 모름 |
|
||||
| 기존 패턴 준수 여부 | 불가 | 기존 코드를 참조하지 않음 |
|
||||
| 사용자 의도 반영 여부 | 불가 | 사용자와 대화 맥락 없음 |
|
||||
|
||||
### 결재 시스템에서의 구체적 영향
|
||||
- PM이 "Backend task 성공, Frontend task 실패"라고 판정할 수는 있지만, **"Backend가 만든 API 응답 구조가 Frontend가 기대하는 것과 다르다"**를 파악할 수 없다
|
||||
- "이 모달의 Combobox가 다른 패널과 UI가 다르다"는 사용자만 판단 가능
|
||||
- "이벤트 훅 시스템의 트리거 타이밍이 잘못됐다"는 전체 아키텍처를 이해해야 판단 가능
|
||||
|
||||
---
|
||||
|
||||
## 8. 안전성 리스크
|
||||
|
||||
### 역사적 사고
|
||||
> "과거 에이전트가 범위 밖 파일 50000줄 삭제하여 2800+ TS 에러 발생"
|
||||
> — user rules
|
||||
|
||||
### 결재 시스템의 리스크
|
||||
수정 대상 파일이 **시스템 핵심 파일**들이다:
|
||||
|
||||
| 파일 | 리스크 |
|
||||
|------|--------|
|
||||
| `improvedButtonActionExecutor.ts` (~1500줄) | 모든 버튼 동작의 핵심. 잘못 건드리면 시스템 전체 버튼 동작 불능 |
|
||||
| `approvalController.ts` (~800줄) | 기존 결재 API 깨질 수 있음 |
|
||||
| `ButtonConfigPanel.tsx` (~600줄) | 화면 디자이너 설정 패널 전체에 영향 |
|
||||
| `v2-approval-step/` (5개 파일) | V2 컴포넌트 레지스트리 손상 가능 |
|
||||
| `AppLayout.tsx` | 전체 레이아웃 메뉴 깨질 수 있음 |
|
||||
| `UserDropdown.tsx` | 사용자 프로필 메뉴 깨질 수 있음 |
|
||||
|
||||
`files` 필드로 범위를 제한하더라도, **에이전트가 `--trust` 모드로 실행**되기 때문에 실제로는 모든 파일에 접근 가능하다:
|
||||
|
||||
```typescript
|
||||
// executor.ts:78
|
||||
const child = spawn(agentPath, ['--model', model, '--print', '--trust'], {
|
||||
```
|
||||
|
||||
code-guard가 일부 보호하지만, **구조적 파괴(잘못된 import 삭제, 함수 시그니처 변경)는 코드 가드가 감지 불가**하다.
|
||||
|
||||
---
|
||||
|
||||
## 9. 종합: 파이프라인이 적합한 경우 vs 부적합한 경우
|
||||
|
||||
### 적합한 경우 (현재 파이프라인)
|
||||
|
||||
| 특성 | 예시 |
|
||||
|------|------|
|
||||
| 새 파일 생성 위주 | 새 CRUD 화면 만들기 |
|
||||
| 독립적 태스크 | 테이블 → API → 화면 순차 |
|
||||
| 패턴이 단순/반복적 | 표준 CRUD, 표준 Form |
|
||||
| 검증이 명확 | 빌드 + API 호출 + 브라우저 기본 확인 |
|
||||
| 컨텍스트 최소 | 기존 시스템 이해 불필요 |
|
||||
|
||||
### 부적합한 경우 (결재 시스템)
|
||||
|
||||
| 특성 | 결재 시스템 해당 여부 |
|
||||
|------|:-------------------:|
|
||||
| 기존 파일 대규모 수정 | 해당 (10+ 파일 수정) |
|
||||
| 크로스도메인 의존성 | 해당 (DB ↔ BE ↔ FE ↔ 기존 시스템) |
|
||||
| 복잡한 비즈니스 로직 | 해당 (5가지 결재 유형, 상태 전이, 이벤트 훅) |
|
||||
| 기존 시스템 깊은 이해 필요 | 해당 (제어관리, 엔티티 조인, 컴포넌트 레지스트리) |
|
||||
| UI/UX 일관성 필수 | 해당 (Combobox, 모달, 설정 패널 패턴 통일) |
|
||||
| 설계 결정이 선행 필요 | 해당 (이벤트 훅 아키텍처, 결재 타입 상태 머신) |
|
||||
| 사용자 피드백 반복 필요 | 해당 (실제로 4회 UI 수정 반복) |
|
||||
|
||||
---
|
||||
|
||||
## 10. 개선 방향 제안
|
||||
|
||||
현재 파이프라인을 결재 시스템 같은 대규모 프로젝트에서 사용하려면 다음이 필요하다:
|
||||
|
||||
### 10.1 컨텍스트 전달 강화
|
||||
- **프로젝트 컨텍스트 파일**: `.cursor/rules/` 수준의 프로젝트 규칙을 에이전트 프롬프트에 동적 주입
|
||||
- **아키텍처 결정 기록**: PM-사용자 간 논의된 설계 결정을 구조화된 형태로 에이전트에 전달
|
||||
- **패턴 레퍼런스 파일**: "이 파일을 참고해서 만들어라"를 task description이 아닌 시스템 차원에서 지원
|
||||
|
||||
### 10.2 파일 컨텍스트 확대
|
||||
- 3000자 절삭 → **전체 파일 전달** 또는 최소 10000자 이상
|
||||
- 관련 파일 자동 탐지 (import 그래프 기반)
|
||||
- 참고 파일(reference files)과 수정 파일(target files) 구분
|
||||
|
||||
### 10.3 에이전트 간 소통 채널
|
||||
- 라운드 내에서도 에이전트 간 **중간 결과 공유** 가능
|
||||
- "Backend가 API 스펙을 먼저 정의 → Frontend가 그 스펙 기반으로 구현" 같은 **단계적 소통**
|
||||
- 질문-응답 프로토콜 (현재 CollabMessage가 있지만 실질적으로 사용 안 됨)
|
||||
|
||||
### 10.4 PM 에이전트 강화
|
||||
- **코드 리뷰 기능**: git diff만 보지 말고 실제 파일을 읽어서 패턴 준수 여부 확인
|
||||
- **아키텍처 검증**: 전체 시스템 구조와의 일관성 검증
|
||||
- **사용자 피드백 루프**: PM이 사용자에게 "이 부분 확인 필요합니다" 알림 가능
|
||||
|
||||
### 10.5 검증 시스템 확장
|
||||
- **비즈니스 로직 검증**: 상태 전이 테스트 (결재 플로우 시나리오 자동 실행)
|
||||
- **UI 일관성 검증**: 스크린샷 비교, 컴포넌트 패턴 분석
|
||||
- **통합 테스트**: 단일 API 호출이 아닌 시나리오 기반 E2E
|
||||
|
||||
### 10.6 안전성 강화
|
||||
- `--trust` 모드 대신 **파일 범위 제한된 실행 모드**
|
||||
- 라운드별 git diff 자동 리뷰 (의도치 않은 파일 변경 감지)
|
||||
- 롤백 자동화 (검증 실패 시 자동 `git checkout`)
|
||||
|
||||
---
|
||||
|
||||
## 부록: 결재 시스템 파이프라인 실행 시 예상 시나리오
|
||||
|
||||
### 시도할 경우 예상되는 실패 패턴
|
||||
|
||||
```
|
||||
Round 1: DB 마이그레이션 (task-1)
|
||||
→ 성공 가능 (신규 파일 생성이므로)
|
||||
|
||||
Round 2: Backend Controller 수정 (task-2)
|
||||
→ approvalController.ts 3000자만 보고 수정 시도
|
||||
→ 기존 함수 구조 파악 실패
|
||||
→ L1 빌드 에러 (import 누락, 타입 불일치)
|
||||
→ 재시도 1: 에러 메시지 보고 고치지만, 기존 패턴과 다른 방식으로 구현
|
||||
→ L3 API 테스트 통과 (기능은 동작)
|
||||
→ 하지만 코드 품질/패턴 불일치 (PM이 감지 불가)
|
||||
|
||||
Round 3: Frontend 모달 수정 (task-4)
|
||||
→ 기존 ApprovalRequestModal 3000자만 보고 수정
|
||||
→ Combobox 패턴 대신 기본 Select 사용 (다른 패널 참고 불가)
|
||||
→ L1 빌드 통과, L5 브라우저 테스트도 기본 동작 통과
|
||||
→ 하지만 UI 일관성 미달 (사용자가 보면 즉시 지적)
|
||||
|
||||
Round 4-6: 이벤트 훅 시스템 (task-7)
|
||||
→ NodeFlowExecutionService 전체 이해 필요한데 3000자만 봄
|
||||
→ 기존 시스템과 연동 불가능한 독립적 구현 생산
|
||||
→ PM이 "빌드 통과했으니 complete" 판정
|
||||
→ 실제로는 기존 제어관리와 전혀 연결 안 됨
|
||||
|
||||
최종: 8/8 task "성공" 판정
|
||||
→ 사용자가 확인: "이거 다 뜯어 고쳐야 하는데?"
|
||||
→ 파이프라인 2시간 + 사용자 수동 수정 3시간 = 5시간 낭비
|
||||
→ PM이 직접 했으면 2~3시간에 끝
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
*작성일: 2026-03-03*
|
||||
*대상: Agent Pipeline v3.0 (`_local/agent-pipeline/`)*
|
||||
*맥락: 결재 시스템 v2 재설계 프로젝트 (`docs/결재시스템_구현_현황.md`)*
|
||||
Reference in New Issue
Block a user