feat: 신규 페이지 구현 및 HR/설정 기능 개선
신규 페이지: - 회계관리: 거래처, 예상비용, 청구서, 발주서 - 게시판: 공지사항, 자료실, 커뮤니티 - 고객센터: 문의/FAQ - 설정: 계정, 알림, 출퇴근, 팝업, 구독, 결제내역 - 리포트 (차트 시각화) - 개발자 테스트 URL 페이지 기능 개선: - HR 직원관리/휴가관리/카드관리 강화 - IntegratedListTemplateV2 확장 - AuthenticatedLayout 패딩 표준화 - 로그인 페이지 UI 개선 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,12 @@
|
||||
# 전체 페이지 테스트 URL 목록
|
||||
|
||||
> 백엔드 메뉴 연동 전 테스트용 직접 접근 URL (Last Updated: 2025-12-08)
|
||||
> 백엔드 메뉴 연동 전 테스트용 직접 접근 URL (Last Updated: 2025-12-19)
|
||||
|
||||
## 🚀 클릭 가능한 웹 페이지
|
||||
|
||||
👉 **[테스트 URL 페이지 열기](http://localhost:3000/ko/dev/test-urls)**
|
||||
|
||||
위 링크에서 모든 URL을 클릭하여 새 탭으로 열 수 있습니다!
|
||||
|
||||
---
|
||||
|
||||
@@ -24,8 +30,6 @@ http://localhost:3000/ko/dashboard
|
||||
|
||||
## 👥 인사관리 (HR)
|
||||
|
||||
### 메인 페이지
|
||||
|
||||
| 페이지 | URL | 상태 |
|
||||
|--------|-----|------|
|
||||
| 부서관리 | `/ko/hr/department-management` | ✅ |
|
||||
@@ -33,6 +37,7 @@ http://localhost:3000/ko/dashboard
|
||||
| 근태관리 | `/ko/hr/attendance-management` | ✅ |
|
||||
| 휴가관리 | `/ko/hr/vacation-management` | ✅ |
|
||||
| 급여관리 | `/ko/hr/salary-management` | ✅ |
|
||||
| **모바일 출퇴근** | `/ko/hr/attendance` | 🧪 테스트중 |
|
||||
|
||||
```
|
||||
http://localhost:3000/ko/hr/department-management
|
||||
@@ -40,30 +45,13 @@ http://localhost:3000/ko/hr/employee-management
|
||||
http://localhost:3000/ko/hr/attendance-management
|
||||
http://localhost:3000/ko/hr/vacation-management
|
||||
http://localhost:3000/ko/hr/salary-management
|
||||
```
|
||||
|
||||
### 사원관리 하위 페이지
|
||||
|
||||
| 페이지 | URL |
|
||||
|--------|-----|
|
||||
| 사원 등록 | `/ko/hr/employee-management/new` |
|
||||
| 사원 상세 | `/ko/hr/employee-management/[id]` |
|
||||
| 사원 수정 | `/ko/hr/employee-management/[id]/edit` |
|
||||
| CSV 업로드 | `/ko/hr/employee-management/csv-upload` |
|
||||
|
||||
```
|
||||
http://localhost:3000/ko/hr/employee-management/new
|
||||
http://localhost:3000/ko/hr/employee-management/1
|
||||
http://localhost:3000/ko/hr/employee-management/1/edit
|
||||
http://localhost:3000/ko/hr/employee-management/csv-upload
|
||||
http://localhost:3000/ko/hr/attendance # 🧪 모바일 출퇴근 (테스트)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 💰 판매관리 (Sales)
|
||||
|
||||
### 메인 페이지
|
||||
|
||||
| 페이지 | URL | 상태 |
|
||||
|--------|-----|------|
|
||||
| 거래처관리 | `/ko/sales/client-management-sales-admin` | ✅ |
|
||||
@@ -76,94 +64,28 @@ http://localhost:3000/ko/sales/quote-management
|
||||
http://localhost:3000/ko/sales/pricing-management
|
||||
```
|
||||
|
||||
### 거래처관리 하위 페이지
|
||||
|
||||
| 페이지 | URL |
|
||||
|--------|-----|
|
||||
| 거래처 등록 | `/ko/sales/client-management-sales-admin/new` |
|
||||
| 거래처 상세 | `/ko/sales/client-management-sales-admin/[id]` |
|
||||
| 거래처 수정 | `/ko/sales/client-management-sales-admin/[id]/edit` |
|
||||
|
||||
```
|
||||
http://localhost:3000/ko/sales/client-management-sales-admin/new
|
||||
http://localhost:3000/ko/sales/client-management-sales-admin/1
|
||||
http://localhost:3000/ko/sales/client-management-sales-admin/1/edit
|
||||
```
|
||||
|
||||
### 견적관리 하위 페이지
|
||||
|
||||
| 페이지 | URL |
|
||||
|--------|-----|
|
||||
| 견적 등록 | `/ko/sales/quote-management/new` |
|
||||
| 견적 상세 | `/ko/sales/quote-management/[id]` |
|
||||
| 견적 수정 | `/ko/sales/quote-management/[id]/edit` |
|
||||
|
||||
```
|
||||
http://localhost:3000/ko/sales/quote-management/new
|
||||
http://localhost:3000/ko/sales/quote-management/1
|
||||
http://localhost:3000/ko/sales/quote-management/1/edit
|
||||
```
|
||||
|
||||
### 단가관리 하위 페이지
|
||||
|
||||
| 페이지 | URL |
|
||||
|--------|-----|
|
||||
| 단가 등록 | `/ko/sales/pricing-management/create` |
|
||||
| 단가 수정 | `/ko/sales/pricing-management/[id]/edit` |
|
||||
|
||||
```
|
||||
http://localhost:3000/ko/sales/pricing-management/create
|
||||
http://localhost:3000/ko/sales/pricing-management/1/edit
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📦 기준정보관리 (Master Data)
|
||||
|
||||
### 품목기준관리
|
||||
|
||||
| 페이지 | URL | 상태 |
|
||||
|--------|-----|------|
|
||||
| 품목 목록 | `/ko/master-data/item-master-data-management` | ✅ |
|
||||
| 품목기준관리 | `/ko/master-data/item-master-data-management` | ✅ |
|
||||
|
||||
```
|
||||
http://localhost:3000/ko/master-data/item-master-data-management
|
||||
```
|
||||
|
||||
### 품목관리 (Items) - 구버전
|
||||
|
||||
| 페이지 | URL |
|
||||
|--------|-----|
|
||||
| 품목 목록 | `/ko/items` |
|
||||
| 품목 등록 | `/ko/items/create` |
|
||||
| 품목 상세 | `/ko/items/[id]` |
|
||||
| 품목 수정 | `/ko/items/[id]/edit` |
|
||||
|
||||
```
|
||||
http://localhost:3000/ko/items
|
||||
http://localhost:3000/ko/items/create
|
||||
http://localhost:3000/ko/items/1
|
||||
http://localhost:3000/ko/items/1/edit
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🏭 생산관리 (Production)
|
||||
|
||||
### 스크린 생산
|
||||
|
||||
| 페이지 | URL |
|
||||
|--------|-----|
|
||||
| 생산 목록 | `/ko/production/screen-production` |
|
||||
| 생산 등록 | `/ko/production/screen-production/create` |
|
||||
| 생산 상세 | `/ko/production/screen-production/[id]` |
|
||||
| 생산 수정 | `/ko/production/screen-production/[id]/edit` |
|
||||
| 페이지 | URL | 상태 |
|
||||
|--------|-----|------|
|
||||
| 스크린 생산 | `/ko/production/screen-production` | ✅ |
|
||||
|
||||
```
|
||||
http://localhost:3000/ko/production/screen-production
|
||||
http://localhost:3000/ko/production/screen-production/create
|
||||
http://localhost:3000/ko/production/screen-production/1
|
||||
http://localhost:3000/ko/production/screen-production/1/edit
|
||||
```
|
||||
|
||||
---
|
||||
@@ -177,6 +99,12 @@ http://localhost:3000/ko/production/screen-production/1/edit
|
||||
| 직급관리 | `/ko/settings/ranks` | ✅ |
|
||||
| 직책관리 | `/ko/settings/titles` | ✅ |
|
||||
| 근무일정 | `/ko/settings/work-schedule` | ✅ |
|
||||
| **출퇴근관리** | `/ko/settings/attendance-settings` | ✅ |
|
||||
| **계좌관리** | `/ko/settings/accounts` | ✅ |
|
||||
| **카드관리** | `/ko/hr/card-management` | 🆕 NEW |
|
||||
| **게시판관리** | `/ko/board/board-management` | 🆕 NEW |
|
||||
| **팝업관리** | `/ko/settings/popup-management` | 🆕 NEW |
|
||||
| **알림설정** | `/ko/settings/notification-settings` | 🆕 NEW |
|
||||
|
||||
```
|
||||
http://localhost:3000/ko/settings/leave-policy
|
||||
@@ -184,10 +112,140 @@ http://localhost:3000/ko/settings/permissions
|
||||
http://localhost:3000/ko/settings/ranks
|
||||
http://localhost:3000/ko/settings/titles
|
||||
http://localhost:3000/ko/settings/work-schedule
|
||||
http://localhost:3000/ko/settings/attendance-settings # 출퇴근관리
|
||||
http://localhost:3000/ko/settings/accounts # 계좌관리
|
||||
http://localhost:3000/ko/settings/notification-settings # 🆕 알림설정
|
||||
http://localhost:3000/ko/hr/card-management # 🆕 카드관리
|
||||
http://localhost:3000/ko/board/board-management # 🆕 게시판관리
|
||||
http://localhost:3000/ko/settings/popup-management # 🆕 팝업관리
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📝 전자결재 (Approval)
|
||||
|
||||
| 페이지 | URL | 상태 |
|
||||
|--------|-----|------|
|
||||
| 기안함 | `/ko/approval/draft` | 🧪 테스트중 |
|
||||
| **결재함** | `/ko/approval/inbox` | ✅ |
|
||||
| **참조함** | `/ko/approval/reference` | ✅ |
|
||||
|
||||
```
|
||||
http://localhost:3000/ko/approval/draft
|
||||
http://localhost:3000/ko/approval/inbox # ✅ 결재함
|
||||
http://localhost:3000/ko/approval/reference # ✅ 참조함
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 💵 회계관리 (Accounting)
|
||||
|
||||
| 페이지 | URL | 상태 |
|
||||
|--------|-----|------|
|
||||
| **거래처 목록** | `/ko/accounting/vendors` | 🆕 NEW |
|
||||
| **매입 목록** | `/ko/accounting/purchase` | ✅ |
|
||||
| **매출 목록** | `/ko/accounting/sales` | ✅ |
|
||||
| **입금 목록** | `/ko/accounting/deposits` | ✅ |
|
||||
| **출금 목록** | `/ko/accounting/withdrawals` | 🆕 NEW |
|
||||
| **어음 목록** | `/ko/accounting/bills` | ✅ |
|
||||
| **거래처원장** | `/ko/accounting/vendor-ledger` | ✅ |
|
||||
| **일일 일보** | `/ko/accounting/daily-report` | ✅ |
|
||||
| **지출 예상 내역서** | `/ko/accounting/expected-expenses` | ✅ |
|
||||
| **미수금 현황** | `/ko/accounting/receivables-status` | ✅ |
|
||||
| **입출금 계좌조회** | `/ko/accounting/bank-transactions` | ✅ |
|
||||
| **카드 내역 조회** | `/ko/accounting/card-transactions` | 🆕 NEW |
|
||||
| **악성채권 추심관리** | `/ko/accounting/bad-debt-collection` | 🆕 NEW |
|
||||
|
||||
```
|
||||
http://localhost:3000/ko/accounting/vendors # 거래처관리
|
||||
http://localhost:3000/ko/accounting/purchase # 매입관리
|
||||
http://localhost:3000/ko/accounting/sales # 매출관리
|
||||
http://localhost:3000/ko/accounting/deposits # 입금관리
|
||||
http://localhost:3000/ko/accounting/withdrawals # 출금관리
|
||||
http://localhost:3000/ko/accounting/bills # 어음관리
|
||||
http://localhost:3000/ko/accounting/vendor-ledger # 거래처원장
|
||||
http://localhost:3000/ko/accounting/daily-report # 일일 일보
|
||||
http://localhost:3000/ko/accounting/expected-expenses # 지출 예상 내역서
|
||||
http://localhost:3000/ko/accounting/receivables-status # 미수금 현황
|
||||
http://localhost:3000/ko/accounting/bank-transactions # 입출금 계좌조회
|
||||
http://localhost:3000/ko/accounting/card-transactions # 카드 내역 조회
|
||||
http://localhost:3000/ko/accounting/bad-debt-collection # 악성채권 추심관리
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📝 게시판 (Board)
|
||||
|
||||
| 페이지 | URL | 상태 |
|
||||
|--------|-----|------|
|
||||
| **게시판 목록** | `/ko/board` | ✅ |
|
||||
|
||||
```
|
||||
http://localhost:3000/ko/board # 게시판 목록
|
||||
```
|
||||
|
||||
> ⚠️ **참고**: 게시판관리는 설정(Settings)에서 관리합니다
|
||||
|
||||
---
|
||||
|
||||
## 📊 보고서 및 분석 (Reports)
|
||||
|
||||
| 페이지 | URL | 상태 |
|
||||
|--------|-----|------|
|
||||
| **종합 경영 분석** | `/ko/reports/comprehensive-analysis` | 🆕 NEW |
|
||||
|
||||
```
|
||||
http://localhost:3000/ko/reports/comprehensive-analysis # 종합 경영 분석
|
||||
```
|
||||
|
||||
> 📋 **사이드바 메뉴**: 종합 경영 분석, 매출현황, 거래현황, 시계열, 거래처분석, 대금회수, 미수금현황, 재고현황, 생산현황, 손익현황, 판관비현황, 고객현황
|
||||
> ℹ️ **참고**: "거래처별 미수금 현황" 버튼 클릭 시 `/ko/accounting/receivables-status`로 이동
|
||||
|
||||
---
|
||||
|
||||
## 👤 계정/회사/구독 (사이드바 별도 메뉴)
|
||||
|
||||
> ⚠️ **참고**: 아래 항목들은 Settings 안이 아닌 **사이드바 루트 레벨**에 별도 메뉴로 존재
|
||||
|
||||
| 페이지 | URL | 상태 |
|
||||
|--------|-----|------|
|
||||
| **계정정보** | `/ko/account-info` | 🆕 NEW |
|
||||
| **회사정보** | `/ko/company-info` | 🆕 NEW |
|
||||
| **구독관리** | `/ko/subscription` | 🆕 NEW |
|
||||
| **결제내역** | `/ko/payment-history` | 🆕 NEW |
|
||||
|
||||
```
|
||||
http://localhost:3000/ko/account-info # 계정정보
|
||||
http://localhost:3000/ko/company-info # 회사정보
|
||||
http://localhost:3000/ko/subscription # 구독관리
|
||||
http://localhost:3000/ko/payment-history # 결제내역
|
||||
```
|
||||
|
||||
> ℹ️ **계정정보**: 탈퇴 버튼은 테넌트 마스터가 아닌 경우에만 활성화, 사용중지 버튼은 테넌트 마스터인 경우에만 활성화
|
||||
> ℹ️ **회사정보**: 테넌트 마스터에게만 표시
|
||||
|
||||
---
|
||||
|
||||
## 📢 고객센터 (Customer Center)
|
||||
|
||||
| 페이지 | URL | 상태 |
|
||||
|--------|-----|------|
|
||||
| **공지사항** | `/ko/customer-center/notices` | ✅ |
|
||||
| **이벤트** | `/ko/customer-center/events` | ✅ |
|
||||
| **FAQ** | `/ko/customer-center/faq` | 🆕 NEW |
|
||||
| **1:1 문의** | `/ko/customer-center/inquiries` | 🆕 NEW |
|
||||
|
||||
```
|
||||
http://localhost:3000/ko/customer-center/notices # 공지사항
|
||||
http://localhost:3000/ko/customer-center/events # 이벤트
|
||||
http://localhost:3000/ko/customer-center/faq # FAQ
|
||||
http://localhost:3000/ko/customer-center/inquiries # 1:1 문의
|
||||
```
|
||||
|
||||
> ℹ️ **고객센터 메뉴**: 공지사항, 이벤트, FAQ, 1:1 문의
|
||||
|
||||
---
|
||||
|
||||
## 📋 전체 URL 한눈에 보기
|
||||
|
||||
### 기본
|
||||
@@ -215,7 +273,6 @@ http://localhost:3000/ko/sales/pricing-management
|
||||
### Master Data
|
||||
```
|
||||
http://localhost:3000/ko/master-data/item-master-data-management
|
||||
http://localhost:3000/ko/items
|
||||
```
|
||||
|
||||
### Production
|
||||
@@ -230,6 +287,54 @@ http://localhost:3000/ko/settings/permissions
|
||||
http://localhost:3000/ko/settings/ranks
|
||||
http://localhost:3000/ko/settings/titles
|
||||
http://localhost:3000/ko/settings/work-schedule
|
||||
http://localhost:3000/ko/settings/attendance-settings # 출퇴근관리
|
||||
http://localhost:3000/ko/settings/accounts # 계좌관리
|
||||
http://localhost:3000/ko/settings/notification-settings # 🆕 알림설정
|
||||
http://localhost:3000/ko/hr/card-management # 🆕 카드관리
|
||||
http://localhost:3000/ko/board/board-management # 🆕 게시판관리
|
||||
http://localhost:3000/ko/settings/popup-management # 🆕 팝업관리
|
||||
```
|
||||
|
||||
### Approval
|
||||
```
|
||||
http://localhost:3000/ko/approval/draft
|
||||
http://localhost:3000/ko/approval/inbox
|
||||
http://localhost:3000/ko/approval/reference
|
||||
```
|
||||
|
||||
### Accounting
|
||||
```
|
||||
http://localhost:3000/ko/accounting/vendors # 거래처관리
|
||||
http://localhost:3000/ko/accounting/purchase # 매입관리
|
||||
http://localhost:3000/ko/accounting/sales # 매출관리
|
||||
http://localhost:3000/ko/accounting/deposits # 입금관리
|
||||
http://localhost:3000/ko/accounting/withdrawals # 출금관리
|
||||
http://localhost:3000/ko/accounting/bills # 어음관리
|
||||
http://localhost:3000/ko/accounting/vendor-ledger # 거래처원장
|
||||
http://localhost:3000/ko/accounting/daily-report # 일일 일보
|
||||
http://localhost:3000/ko/accounting/expected-expenses # 지출 예상 내역서
|
||||
http://localhost:3000/ko/accounting/receivables-status # 미수금 현황
|
||||
http://localhost:3000/ko/accounting/bank-transactions # 입출금 계좌조회
|
||||
http://localhost:3000/ko/accounting/card-transactions # 🆕 카드 내역 조회
|
||||
http://localhost:3000/ko/accounting/bad-debt-collection # 악성채권 추심관리
|
||||
```
|
||||
|
||||
### Board
|
||||
```
|
||||
http://localhost:3000/ko/board # 게시판 목록
|
||||
```
|
||||
|
||||
### Reports
|
||||
```
|
||||
http://localhost:3000/ko/reports/comprehensive-analysis # 종합 경영 분석
|
||||
```
|
||||
|
||||
### Customer Center
|
||||
```
|
||||
http://localhost:3000/ko/customer-center/notices # 공지사항
|
||||
http://localhost:3000/ko/customer-center/events # 이벤트
|
||||
http://localhost:3000/ko/customer-center/faq # FAQ
|
||||
http://localhost:3000/ko/customer-center/inquiries # 1:1 문의
|
||||
```
|
||||
|
||||
---
|
||||
@@ -251,7 +356,6 @@ http://localhost:3000/ko/settings/work-schedule
|
||||
|
||||
// Master Data
|
||||
'/master-data/item-master-data-management'
|
||||
'/items'
|
||||
|
||||
// Production
|
||||
'/production/screen-production'
|
||||
@@ -262,6 +366,50 @@ http://localhost:3000/ko/settings/work-schedule
|
||||
'/settings/ranks'
|
||||
'/settings/titles'
|
||||
'/settings/work-schedule'
|
||||
'/settings/attendance-settings' // 출퇴근관리
|
||||
'/settings/accounts' // 계좌관리
|
||||
'/settings/notification-settings' // 알림설정 (🆕 NEW)
|
||||
'/hr/card-management' // 카드관리 (🆕 NEW)
|
||||
'/board/board-management' // 게시판관리 (🆕 NEW)
|
||||
'/settings/popup-management' // 팝업관리 (🆕 NEW)
|
||||
|
||||
// 계정/회사/구독 (사이드바 루트 레벨 별도 메뉴)
|
||||
'/account-info' // 계정정보 (🆕 NEW)
|
||||
'/company-info' // 회사정보 (🆕 NEW)
|
||||
'/subscription' // 구독관리 (🆕 NEW)
|
||||
'/payment-history' // 결제내역 (🆕 NEW)
|
||||
|
||||
// Approval (전자결재)
|
||||
'/approval/draft' // 기안함
|
||||
'/approval/inbox' // 결재함
|
||||
'/approval/reference' // 참조함
|
||||
|
||||
// Accounting (회계관리)
|
||||
'/accounting/vendors' // 거래처관리
|
||||
'/accounting/purchase' // 매입관리
|
||||
'/accounting/sales' // 매출관리
|
||||
'/accounting/deposits' // 입금관리
|
||||
'/accounting/withdrawals' // 출금관리
|
||||
'/accounting/bills' // 어음관리
|
||||
'/accounting/vendor-ledger' // 거래처원장
|
||||
'/accounting/daily-report' // 일일 일보
|
||||
'/accounting/expected-expenses' // 지출 예상 내역서
|
||||
'/accounting/receivables-status' // 미수금 현황
|
||||
'/accounting/bank-transactions' // 입출금 계좌조회
|
||||
'/accounting/card-transactions' // 카드 내역 조회
|
||||
'/accounting/bad-debt-collection' // 악성채권 추심관리
|
||||
|
||||
// Board (게시판)
|
||||
'/board' // 게시판 목록
|
||||
|
||||
// Reports (보고서 및 분석)
|
||||
'/reports/comprehensive-analysis' // 종합 경영 분석
|
||||
|
||||
// Customer Center (고객센터)
|
||||
'/customer-center/notices' // 공지사항
|
||||
'/customer-center/events' // 이벤트
|
||||
'/customer-center/faq' // FAQ (🆕 NEW)
|
||||
'/customer-center/inquiries' // 1:1 문의 (🆕 NEW)
|
||||
```
|
||||
|
||||
---
|
||||
@@ -269,4 +417,4 @@ http://localhost:3000/ko/settings/work-schedule
|
||||
## 작성일
|
||||
|
||||
- 최초 작성: 2025-12-06
|
||||
- 최종 업데이트: 2025-12-08 (전체 페이지 통합)
|
||||
- 최종 업데이트: 2025-12-19 (하위 페이지 정리, 리스트 페이지만 유지)
|
||||
|
||||
@@ -19,6 +19,9 @@ claudedocs/
|
||||
├── hr/ # 👥 인사관리 (부서/사원)
|
||||
├── item-master/ # 📦 품목기준관리
|
||||
├── sales/ # 💰 판매관리 (견적/거래처)
|
||||
├── accounting/ # 💳 회계관리 (매입/매출/출금)
|
||||
├── board/ # 📝 게시판 관리
|
||||
├── settings/ # ⚙️ 설정 관리 (NEW)
|
||||
├── dashboard/ # 📊 대시보드 & 사이드바
|
||||
├── api/ # 🔌 API 통합
|
||||
├── guides/ # 📚 범용 가이드
|
||||
@@ -50,6 +53,7 @@ claudedocs/
|
||||
|
||||
| 파일 | 설명 |
|
||||
|------|------|
|
||||
| `[IMPL-2025-12-16] mobile-attendance.md` | 🔴 **NEW** - 모바일 출퇴근 시스템 (카카오맵 GPS 기반, MVP) |
|
||||
| `[IMPL-2025-12-05] department-management-checklist.md` | ✅ **완료** - 부서관리 구현 체크리스트 (무제한 트리구조) |
|
||||
| `[IMPL-2025-12-05] employee-management-checklist.md` | ✅ **완료** - 사원관리 구현 체크리스트 |
|
||||
| `[IMPL-2025-12-06] vacation-management-checklist.md` | ✅ **완료** - 휴가관리 구현 체크리스트 |
|
||||
@@ -130,7 +134,8 @@ claudedocs/
|
||||
|
||||
| 파일 | 설명 |
|
||||
|------|------|
|
||||
| `[GUIDE] large-file-handling-strategy.md` | 🔴 **NEW** - 대용량 파일 처리 전략 (100MB+ CAD 도면, 청크 업로드, 스트리밍 다운로드) |
|
||||
| `[GUIDE-2025-12-16] options-vs-flattened-data.md` | 🔴 **NEW** - options vs 평탄화 데이터 패턴 (API 응답 매핑 시 options 직접 파싱 금지) |
|
||||
| `[GUIDE] large-file-handling-strategy.md` | 대용량 파일 처리 전략 (100MB+ CAD 도면, 청크 업로드, 스트리밍 다운로드) |
|
||||
| `[FIX-2025-12-05] radix-ui-select-controlled-mode-bug.md` | ⭐ **핵심** - Radix UI Select 버그 해결 (Edit 모드 값 표시 안됨 → key prop 강제 리마운트) |
|
||||
| `i18n-usage-guide.md` | 다국어 사용 가이드 |
|
||||
| `form-validation-guide.md` | 폼 유효성 검사 |
|
||||
@@ -153,6 +158,32 @@ claudedocs/
|
||||
|
||||
---
|
||||
|
||||
## 💳 accounting/ - 회계관리 (거래처/매입/매출/출금)
|
||||
|
||||
| 파일 | 설명 |
|
||||
|------|------|
|
||||
| `[IMPL-2025-12-18] vendor-management-checklist.md` | 🔴 **NEW** - 거래처관리 구현 체크리스트 (리스트 + 상세 페이지) |
|
||||
| `[IMPL-2025-12-18] purchase-management.md` | 매입관리 페이지 구현 (리스트 + 상세 모달) |
|
||||
|
||||
---
|
||||
|
||||
## 📝 board/ - 게시판 관리
|
||||
|
||||
| 파일 | 설명 |
|
||||
|------|------|
|
||||
| `[PLAN-2025-12-19] board-management-implementation.md` | 🔴 **NEW** - 게시판 구현 계획서 (리스트/등록/상세/댓글, TipTap 에디터) |
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ settings/ - 설정 관리
|
||||
|
||||
| 파일 | 설명 |
|
||||
|------|------|
|
||||
| `[IMPL-2025-12-19] company-info.md` | 🔴 **NEW** - 회사정보 구현 (폼 기반, 회사 추가 팝업) |
|
||||
| `[IMPL-2025-12-19] popup-management.md` | 팝업관리 구현 (리스트/등록/상세/수정, RichTextEditor) |
|
||||
|
||||
---
|
||||
|
||||
## 📁 archive/ - 레거시/완료된 문서
|
||||
|
||||
완료되거나 더 이상 활성화되지 않은 문서들. 참조용으로 보관.
|
||||
|
||||
65
claudedocs/accounting/[IMPL-2025-12-18] bill-management.md
Normal file
65
claudedocs/accounting/[IMPL-2025-12-18] bill-management.md
Normal file
@@ -0,0 +1,65 @@
|
||||
# 어음관리 (Bill Management) 구현
|
||||
|
||||
## 스크린샷 분석
|
||||
|
||||
### 리스트 화면
|
||||
- 상단: 일괄등록 버튼, 날짜범위 선택, 상태 탭 (전체, 전일, 오늘, 미결, 수취, 우등록)
|
||||
- 통계 카드: 4개 (건수 표시)
|
||||
- 테이블 컬럼: No, 어음번호, 구분, 거래처, 금액, 만기일, 이유, 추수, 메모, 상태
|
||||
- 필터: 거래처, 상태
|
||||
|
||||
### 상세/수정 화면
|
||||
- 타이틀: "어음 상세"
|
||||
- 버튼: view 모드 [목록, 삭제, 수정] / edit 모드 [취소, 저장]
|
||||
- 기본 정보:
|
||||
- 어음번호 (Input)
|
||||
- 구분 (Select: 수취/발행)
|
||||
- 거래처 (Select)
|
||||
- 금액 (Input)
|
||||
- 발행일 (Date)
|
||||
- 만기일 (Date)
|
||||
- 상태 (Select - 구분에 따라 옵션 변경)
|
||||
- 비고 (Input)
|
||||
- 차수 관리 섹션:
|
||||
- 테이블: 일자, 금액, 비고
|
||||
- [+ 추가] 버튼
|
||||
|
||||
### 타입 정의
|
||||
- **구분**: 수취, 발행
|
||||
- **상태 (수취)**: 보관중, 만기입금(7일전), 만기결과, 결제완료, 부도
|
||||
- **상태 (발행)**: 보관중, 만기입금(7일전), 추심의뢰, 추심완료, 추소중, 부도
|
||||
|
||||
---
|
||||
|
||||
## 체크리스트
|
||||
|
||||
### Phase 1: 기본 구조
|
||||
- [x] types.ts 생성 (타입, 상수 정의)
|
||||
- [x] 폴더 구조 생성 (BillManagement/)
|
||||
|
||||
### Phase 2: 리스트 화면
|
||||
- [x] index.tsx 생성 (리스트 컴포넌트)
|
||||
- [x] 통계 카드 구현
|
||||
- [x] 테이블 렌더링 구현
|
||||
- [x] 필터 및 정렬 구현
|
||||
|
||||
### Phase 3: 상세/수정 화면
|
||||
- [x] BillDetail.tsx 생성
|
||||
- [x] 기본 정보 폼 구현
|
||||
- [x] 차수 관리 섹션 구현
|
||||
- [x] view/edit 모드 분기 처리
|
||||
|
||||
### Phase 4: 라우팅
|
||||
- [x] page.tsx 파일 생성 (리스트, 상세, 등록)
|
||||
- [x] 네비게이션 패턴 적용 (?mode=edit)
|
||||
|
||||
### Phase 5: 검증
|
||||
- [x] 빌드 테스트 ✅
|
||||
- [ ] 기능 확인 (사용자 확인 필요)
|
||||
|
||||
---
|
||||
|
||||
## 참고 패턴
|
||||
- 입금관리 (DepositManagement) 구조 참고
|
||||
- IntegratedListTemplateV2 사용
|
||||
- PageLayout + PageHeader 패턴
|
||||
@@ -0,0 +1,38 @@
|
||||
# 지출 예상 내역서 구현 체크리스트
|
||||
|
||||
## 현재 세션 작업 (2025-12-18)
|
||||
|
||||
### 1. 테이블 필터 수정
|
||||
- [x] 1.1 첫번째 필터: 전체 → 거래처 목록으로 변경 ✅
|
||||
- 현재: 거래유형 필터 (매입, 선급금, 가지급금 등)
|
||||
- 변경: 거래처 필터 (전체, 거래처1, 거래처2...)
|
||||
- [x] 1.2 두번째 필터: 정렬 옵션 축소 ✅
|
||||
- 현재: 최신순, 등록순, 지급일 빠른순, 지급일 느린순, 금액 높은순, 금액 낮은순
|
||||
- 변경: 최신순, 등록순 (2개만)
|
||||
|
||||
### 2. 예상 지급일 변경 팝업
|
||||
- [x] 2.1 팝업 다이얼로그 생성 ✅
|
||||
- 헤더: "예상 지급일 변경" + X 닫기 버튼
|
||||
- [x] 2.2 선택 항목 요약 영역 ✅
|
||||
- 항목명 외 N (선택된 항목 수)
|
||||
- 총 금액 표시 (합계)
|
||||
- [x] 2.3 예상 지급일 선택 ✅
|
||||
- 라벨: "예상 지급일"
|
||||
- 공용 달력 컴포넌트 사용 (Calendar + Popover)
|
||||
- [x] 2.4 버튼 영역 ✅
|
||||
- 취소 버튼
|
||||
- 저장 버튼 (날짜 미선택 시 비활성화)
|
||||
|
||||
### 3. 전자결재 버튼 기능
|
||||
- [x] 3.1 버튼 클릭 시 페이지 이동 ✅
|
||||
- 이동 경로: `/ko/approval/document-write/expected-expense` (문서 작성_지출 예상 내역서)
|
||||
- 항목 선택 필수 (미선택 시 버튼 비활성화)
|
||||
|
||||
---
|
||||
|
||||
## 참고 스크린샷
|
||||
- 예상 지급일 변경 팝업: `스크린샷 2025-12-18 오후 4.39.35.png`
|
||||
- 필터 위치 참고: `스크린샷 2025-12-18 오후 4.19.33.png`, `4.20.20.png`
|
||||
|
||||
## 테스트 URL
|
||||
- http://localhost:3000/ko/accounting/expected-expenses
|
||||
111
claudedocs/accounting/[IMPL-2025-12-18] purchase-management.md
Normal file
111
claudedocs/accounting/[IMPL-2025-12-18] purchase-management.md
Normal file
@@ -0,0 +1,111 @@
|
||||
# [IMPL-2025-12-18] 매입관리 페이지 구현
|
||||
|
||||
## 개요
|
||||
회계관리 > 매입관리 페이지 구현 (리스트 + 상세)
|
||||
|
||||
## 참고자료
|
||||
- 기안함 리스트 페이지: `src/components/approval/DraftBox/index.tsx`
|
||||
- 공통 템플릿: `IntegratedListTemplateV2`
|
||||
- 공통 컴포넌트: `DateRangeSelector`, `ListMobileCard`
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: 리스트 페이지
|
||||
|
||||
### 1.1 페이지 구조
|
||||
- [ ] 라우트 생성: `/accounting/purchase`
|
||||
- [ ] 컴포넌트 생성: `src/components/accounting/PurchaseManagement/index.tsx`
|
||||
- [ ] 타입 정의: `src/components/accounting/PurchaseManagement/types.ts`
|
||||
|
||||
### 1.2 상단 영역
|
||||
- [ ] 날짜 범위 선택 (DateRangeSelector)
|
||||
- [ ] 탭 버튼: 담대조건, 진행중, 당일, 이달, 이번, 미결
|
||||
|
||||
### 1.3 통계 카드 (4개)
|
||||
- [ ] 매입금액 합계 (원)
|
||||
- [ ] 출금액 합계 (원)
|
||||
- [ ] 매입 건수
|
||||
- [ ] 미결 건수
|
||||
|
||||
### 1.4 필터 셀렉트 박스
|
||||
- [ ] 부가세여부 필터 (다중 선택): 부가세여부, 상품매입, 오주경비, 소모품비, 수선비, 원재료비, 사무용품비 등
|
||||
- [ ] 거래처 필터 (검색 + 다중 선택)
|
||||
- [ ] 증빙 필터 (다중 선택): 증빙유형 목록
|
||||
|
||||
### 1.5 테이블 컬럼
|
||||
| 컬럼 | 설명 |
|
||||
|------|------|
|
||||
| No | 순번 |
|
||||
| 매입일자 | 매입 등록일 |
|
||||
| 매입금액 | 금액 |
|
||||
| 거래처 | 거래처명 |
|
||||
| 출금액 | 실제 출금액 |
|
||||
| 부가세 | 부가세 금액 |
|
||||
| 매입유형 | 유형 분류 |
|
||||
| 증빙유형 | 세금계산서 등 |
|
||||
|
||||
### 1.6 기능
|
||||
- [ ] 매입 자동 등록: 지출예상내역서 승인 완료 시 자동 등록
|
||||
- [ ] 매입/매출등록 Alert 표시 (API 연동 예정)
|
||||
- [ ] 일람표/거래처원장 연계 출력
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: 상세 페이지 (모달)
|
||||
|
||||
### 2.1 기본 정보 섹션
|
||||
- [ ] 근거 문서명: 품의서 또는 지출결의서 표시
|
||||
- [ ] 결재 버튼: 클릭 시 매입 문서 상세 팝업
|
||||
- [ ] 예상 비용 표시: 품의서/지출결의서 예상/총 비용
|
||||
|
||||
### 2.2 매입 정보 섹션
|
||||
- [ ] 매입일자: 문서번호 + 상세조회 아이콘
|
||||
- [ ] 출금계좌 셀렉트 박스: 등록된 계좌 목록 (계좌번호 마지막 4자리 + 별명)
|
||||
- [ ] 거래처 셀렉트 박스: 검색 기능
|
||||
- [ ] 매입 유형 셀렉트 박스: 부자재매입, 상품매입, 오주경비, 소모품비, 수선비, 원재료비, 사무용품비, 임차료, 수도광열비, 통신비, 차량유지비, 잡비, 보험료, 기타경비, 미상담
|
||||
|
||||
### 2.3 품목 정보 섹션
|
||||
- [ ] 테이블: 품목명, 수량, 단가, 공급가액, 부가세, 적요
|
||||
- [ ] 행 추가/삭제 기능
|
||||
- [ ] 합계 표시
|
||||
|
||||
### 2.4 세금계산서 섹션
|
||||
- [ ] 세금계산서 수취 토글 버튼
|
||||
- [ ] 토글 시 미수취/수취완료 상태 변경
|
||||
- [ ] 수취 완료 후 완료 상태로 변경
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: 연동 및 마무리
|
||||
- [ ] 전자결재 시스템 연동 (지출예상내역서 승인 → 매입 자동 등록)
|
||||
- [ ] API 연동 준비 (비포템 API 자동 등록 예정)
|
||||
- [ ] 일람표/거래처원장 출력 기능
|
||||
|
||||
---
|
||||
|
||||
## 파일 구조
|
||||
```
|
||||
src/
|
||||
├── app/[locale]/(protected)/accounting/
|
||||
│ └── purchase/
|
||||
│ └── page.tsx
|
||||
├── components/accounting/
|
||||
│ └── PurchaseManagement/
|
||||
│ ├── index.tsx # 리스트 페이지
|
||||
│ ├── types.ts # 타입 정의
|
||||
│ └── PurchaseDetailModal.tsx # 상세 모달
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 진행 상태
|
||||
- [x] 요구사항 분석 완료
|
||||
- [x] 계획서 작성 완료
|
||||
- [x] Phase 1 완료 (리스트 페이지)
|
||||
- [x] Phase 2 완료 (상세 모달)
|
||||
- [ ] Phase 3 대기 (API 연동)
|
||||
|
||||
## 테스트 URL
|
||||
```
|
||||
http://localhost:3000/ko/accounting/purchase
|
||||
```
|
||||
@@ -0,0 +1,73 @@
|
||||
# 미수금 현황 페이지 구현 체크리스트
|
||||
|
||||
## 기본 정보
|
||||
- **생성일**: 2025-12-18
|
||||
- **경로**: `/ko/accounting/receivables-status`
|
||||
- **참고 페이지**: 매출관리, 거래처원장
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: 기본 구조 설정
|
||||
|
||||
- [x] 페이지 라우트 생성 (`src/app/[locale]/(protected)/accounting/receivables-status/page.tsx`)
|
||||
- [x] 컴포넌트 디렉토리 생성 (`src/components/accounting/ReceivablesStatus/`)
|
||||
- [x] 메인 컴포넌트 생성 (`index.tsx`)
|
||||
- [x] 타입 정의 파일 생성 (`types.ts`)
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: 레이아웃 구현
|
||||
|
||||
- [x] DateRangeSelector 공통 달력 적용
|
||||
- [x] 프리셋 버튼 (당해년도, 전전월, 전월, 당월, 어제, 오늘)
|
||||
- [x] 엑셀 다운로드 버튼
|
||||
- [x] 저장 버튼 (엑셀 다운로드 아래)
|
||||
- [x] 검색창 (거래처 검색)
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: 테이블 구현 (특수 구조)
|
||||
|
||||
테이블 구조 (스크린샷 기준):
|
||||
- 컬럼: 거래처/연체, 구분, 1월~12월, 합계
|
||||
- 그룹핑: 거래처별로 묶이고 각 거래처 아래 구분 (5개: 매출, 입금, 어음, 미수금, 메모)
|
||||
|
||||
- [x] 거래처별 그룹핑 테이블 구조 (rowSpan=5 사용)
|
||||
- [x] 월별 컬럼 (1월~12월 + 합계)
|
||||
- [x] 구분 행 (매출, 입금, 어음, 미수금, 메모) - 5개 카테고리
|
||||
- [x] 연체 토글 (거래처/연체 컬럼 내)
|
||||
- [x] 연체 영역 전체 하이라이트 (토글 ON 시 해당 월 전체 빨간 배경)
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: 토글 기능
|
||||
|
||||
스크린샷 Description 기준:
|
||||
- ON: 연체 상태로 표시, 연체일수 시작
|
||||
- OFF: 정상 상태
|
||||
- 거래처 상세에서 연체 설정과 연동
|
||||
|
||||
- [x] Switch 컴포넌트로 연체 토글 구현
|
||||
- [x] 토글 상태에 따른 UI 변화
|
||||
- [x] 연체 상태 표시 로직
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: 추가 기능
|
||||
|
||||
- [x] Mock 데이터 생성
|
||||
- [x] 합계 행 계산
|
||||
- [ ] 모바일 카드 뷰 (추후 필요시)
|
||||
- [x] 반응형 레이아웃 (overflow-x-auto)
|
||||
|
||||
---
|
||||
|
||||
## 진행 상태
|
||||
- **현재 단계**: 완료
|
||||
- **마지막 업데이트**: 2025-12-18
|
||||
|
||||
---
|
||||
|
||||
## 참고 사항
|
||||
- Description 영역 (수취 어음 등록 시 표시, 메모 입력박스)은 현재 스코프에서 제외
|
||||
- 기본 기능 먼저 구현 후 추가 기능 논의
|
||||
129
claudedocs/accounting/[IMPL-2025-12-18] vendor-ledger.md
Normal file
129
claudedocs/accounting/[IMPL-2025-12-18] vendor-ledger.md
Normal file
@@ -0,0 +1,129 @@
|
||||
# [IMPL-2025-12-18] 거래처원장 (Vendor Ledger) 구현
|
||||
|
||||
## 개요
|
||||
- **화면명**: 거래처원장
|
||||
- **경로**: 회계관리 > 거래처원장
|
||||
- **구성**: 리스트 페이지 + 상세 페이지
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: 리스트 페이지 (VendorLedger/index.tsx)
|
||||
|
||||
### 1.1 헤더 영역
|
||||
- [ ] 제목: "거래처원장"
|
||||
- [ ] 설명: "거래처별 기간 내역을 조회합니다."
|
||||
- [ ] 기간 선택기: 2025-09-01 ~ 2025-09-03 형식
|
||||
- [ ] 기간 버튼: 당해년도, 전년월, 전월, 당월, 어제, 오늘
|
||||
- [ ] 엑셀 다운로드 버튼
|
||||
|
||||
### 1.2 요약 카드 (4개)
|
||||
- [ ] 전기 이월: 3,123,000원
|
||||
- [ ] 매출: 3,123,000원
|
||||
- [ ] 수금: 3,123,000원
|
||||
- [ ] 잔액: 3,123,000원
|
||||
|
||||
### 1.3 테이블 영역
|
||||
- [ ] 검색 필드
|
||||
- [ ] 총 N건 표시
|
||||
- [ ] 테이블 컬럼:
|
||||
- No.
|
||||
- 거래처명
|
||||
- 이월잔액
|
||||
- 매출
|
||||
- 수금
|
||||
- 잔액
|
||||
- 결제일
|
||||
- [ ] 합계 행 (테이블 하단)
|
||||
- [ ] 행 클릭 시 상세 페이지 이동
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: 상세 페이지 (VendorLedger/VendorLedgerDetail.tsx)
|
||||
|
||||
### 2.1 헤더 영역
|
||||
- [ ] 제목: "거래처원장 상세 (거래명세서별)"
|
||||
- [ ] 설명: "거래처 상세 내역을 조회합니다."
|
||||
- [ ] 기간 선택기: 2025-09-01 ~ 2025-09-03
|
||||
- [ ] 기간 버튼: 당해년도, 전년월, 전월, 당월, 어제, 오늘
|
||||
- [ ] PDF 다운로드 버튼
|
||||
|
||||
### 2.2 거래처 정보 섹션 (2열 레이아웃)
|
||||
- [ ] 좌측 열:
|
||||
- 회사명: [값]
|
||||
- 사업자등록번호: 123-12-12345
|
||||
- 전화번호: 02-1234-1234
|
||||
- 팩스: 02-1234-1236
|
||||
- 주소: 주소영
|
||||
- [ ] 우측 열:
|
||||
- 기간: 2025-01-01 ~ 2025-12-31
|
||||
- 대표자: 홍길동
|
||||
- 모바일: 02-1234-1235
|
||||
- 이메일: abc@email.com
|
||||
|
||||
### 2.3 판매/수금 내역 테이블
|
||||
- [ ] 컬럼: 일자, 적요, 판매, 수금, 잔액, 작업
|
||||
- [ ] 이월잔액 행 (첫 행, 적요에 "이월잔액")
|
||||
- [ ] 거래 행 (◆ 아이콘 + 날짜)
|
||||
- 클릭 시 어음 상세 화면 이동
|
||||
- [ ] 거래명세서 행 (클릭 시 문서 상세 팝업)
|
||||
- [ ] 품목명 행 (세금계산서 미발행 시 노란색 하이라이트)
|
||||
- [ ] 누계 행 (※ 123건 누계 (VAT 포함) 형식)
|
||||
- [ ] 월별 계 행 (회색 배경, 예: "2025/01 계", "2025/09 계")
|
||||
- [ ] 작업 컬럼: 수정 아이콘 (✏️)
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: 타입 및 라우트
|
||||
|
||||
### 3.1 types.ts
|
||||
- [ ] VendorLedgerItem 인터페이스
|
||||
- [ ] VendorLedgerDetail 인터페이스
|
||||
- [ ] TransactionEntry 인터페이스
|
||||
|
||||
### 3.2 라우트 설정
|
||||
- [ ] `/ko/accounting/vendor-ledger` - 리스트 페이지
|
||||
- [ ] `/ko/accounting/vendor-ledger/[id]` - 상세 페이지
|
||||
|
||||
---
|
||||
|
||||
## 스크린샷 상세 분석
|
||||
|
||||
### 리스트 페이지 테이블 데이터 예시
|
||||
| No. | 거래처명 | 이월잔액 | 매출 | 수금 | 잔액 | 결제일 |
|
||||
|-----|---------|---------|------|------|------|--------|
|
||||
| 7 | 회사명 | -100,000 | | | | |
|
||||
| 6 | 회사명 | 10,000,000 | 10,000,000 | 10,000,000 | | |
|
||||
| 5 | 회사명 | 10,000,000 | | | | |
|
||||
| ... | | | | | | |
|
||||
| **합계** | | 10,000,000 | 10,000,000 | 10,000,000 | 10,000,000 | |
|
||||
|
||||
### 상세 페이지 판매/수금 내역 예시
|
||||
| 일자 | 적요 | 판매 | 수금 | 잔액 | 작업 |
|
||||
|------|------|------|------|------|------|
|
||||
| | 이월잔액 | 10,000,000 | | | |
|
||||
| ◆ 2025-01-01 | 수취 어음 (만기 1/5) | | 3,000,000 | 10,000,000 | ✏️ |
|
||||
| ◆ 2025-01-05 | 어음 회수 | | 3,000,000 | | |
|
||||
| **2025/01 계** | | | | | |
|
||||
| ◆ 2025-09-25 | 매출 입력 | 1,000,000 | | | |
|
||||
| | 품목명 | **🟡 500,000** | | | |
|
||||
| | ※ 123건 누계 (VAT 포함) | 1,000,000 | | 1,000,000 | |
|
||||
| **2025/09 계** | | 1,000,000 | 8,000,000 | | |
|
||||
|
||||
---
|
||||
|
||||
## 작업 진행 상황
|
||||
- [x] Phase 1: 리스트 페이지 구현
|
||||
- [x] Phase 2: 상세 페이지 구현
|
||||
- [x] Phase 3: 타입 및 라우트 설정
|
||||
- [x] Phase 4: 테스트 및 검증
|
||||
|
||||
## 생성된 파일
|
||||
1. `src/components/accounting/VendorLedger/types.ts` - 타입 정의
|
||||
2. `src/components/accounting/VendorLedger/index.tsx` - 리스트 페이지
|
||||
3. `src/components/accounting/VendorLedger/VendorLedgerDetail.tsx` - 상세 페이지
|
||||
4. `src/app/[locale]/(protected)/accounting/vendor-ledger/page.tsx` - 리스트 라우트
|
||||
5. `src/app/[locale]/(protected)/accounting/vendor-ledger/[id]/page.tsx` - 상세 라우트
|
||||
|
||||
## 접속 URL
|
||||
- 리스트: `/ko/accounting/vendor-ledger`
|
||||
- 상세: `/ko/accounting/vendor-ledger/[id]`
|
||||
@@ -0,0 +1,287 @@
|
||||
# 거래처 관리 (Vendor Management) 구현 체크리스트
|
||||
|
||||
> **상태**: ✅ 완료 (2025-12-18)
|
||||
> **리스트 수정**: ✅ 완료 (2025-12-18) - 필터/액션버튼/신규등록 수정
|
||||
|
||||
## 개요
|
||||
- **경로**: `/accounting/vendors` (리스트), `/accounting/vendors/[id]` (상세), `/accounting/vendors/new` (신규등록)
|
||||
- **기능**: 거래처 등록, 조회, 수정, 삭제
|
||||
- **참고**: 스크린샷 4장 (리스트 1장, 상세 3장)
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: 타입 및 상수 정의 ✅
|
||||
|
||||
### 1.1 types.ts 생성
|
||||
- [x] 거래처 구분 타입 (VendorCategory): `sales` | `purchase` | `both`
|
||||
- [x] 신용등급 타입 (CreditRating): `AAA` | `AA` | `A` | `BBB` | `BB` | `B` | `CCC` | `CC` | `C` | `D`
|
||||
- [x] 거래등급 타입 (TransactionGrade): `A` | `B` | `C` | `D` | `E`
|
||||
- [x] 악성채권 상태 타입 (BadDebtStatus): `none` | `normal` | `warning`
|
||||
- [x] 정렬 옵션 타입 (SortOption)
|
||||
- [x] 거래처 인터페이스 (Vendor)
|
||||
- id, vendorCode, businessNumber
|
||||
- vendorName, representativeName
|
||||
- category (sales/purchase/both)
|
||||
- businessType, businessCategory
|
||||
- address (zipCode, address1, address2)
|
||||
- phone, mobile, fax, email
|
||||
- managerName, managerPhone, systemManager
|
||||
- logoUrl
|
||||
- purchasePaymentDay, salesPaymentDay
|
||||
- creditRating, transactionGrade
|
||||
- taxInvoiceEmail, bankName, accountNumber, accountHolder
|
||||
- outstandingAmount, overdueAmount, overdueDays
|
||||
- unpaidAmount, badDebtStatus
|
||||
- overdueToggle, badDebtToggle
|
||||
- memos: Memo[]
|
||||
- createdAt, updatedAt
|
||||
|
||||
### 1.2 상수 정의
|
||||
- [x] VENDOR_CATEGORY_OPTIONS (구분 필터)
|
||||
- [x] CREDIT_RATING_OPTIONS (신용등급 필터)
|
||||
- [x] TRANSACTION_GRADE_OPTIONS (거래등급 필터)
|
||||
- [x] BAD_DEBT_STATUS_OPTIONS (악성채권 필터)
|
||||
- [x] SORT_OPTIONS (정렬 옵션)
|
||||
- [x] PAYMENT_DAY_OPTIONS (결제일 1~31일)
|
||||
- [x] BANK_OPTIONS (은행 목록)
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: 리스트 페이지 구현 ✅
|
||||
|
||||
### 2.1 페이지 라우트 생성
|
||||
- [x] `/src/app/[locale]/(protected)/accounting/vendors/page.tsx` 생성
|
||||
|
||||
### 2.2 VendorManagement 컴포넌트
|
||||
- [x] `/src/components/accounting/VendorManagement/index.tsx` 생성
|
||||
|
||||
#### 2.2.1 상단 통계 카드 (3개)
|
||||
| 카드 | 값 | 아이콘 색상 |
|
||||
|------|-----|------------|
|
||||
| 전체 거래처 | {count}개 | blue |
|
||||
| 매출 거래처 | {count}개 | green |
|
||||
| 매입 거래처 | {count}개 | orange |
|
||||
|
||||
#### 2.2.2 필터 조건 (5개 셀렉트박스)
|
||||
| 필터 | 옵션 | 기본값 |
|
||||
|------|------|--------|
|
||||
| 구분 | 전체, 매출, 매입, 매입매출 | 전체 |
|
||||
| 신용등급 | 전체, AAA, AA, A, BBB, BB, B, CCC, CC, C, D | 전체 |
|
||||
| 거래등급 | 전체, A(우수), B(양호), C(보통), D(주의), E(위험) | 전체 |
|
||||
| 악성채권 | 전체, 미설정, 정상 | 전체 |
|
||||
| 정렬 | 최신순, 등록순, 거래처명 오름차순, 거래처명 내림차순, 미수금 높은순, 미수금 낮은순 | 최신순 |
|
||||
|
||||
#### 2.2.3 테이블 컬럼 (체크박스 + 9개)
|
||||
| 순서 | 컬럼명 | 정렬 | 비고 |
|
||||
|------|--------|------|------|
|
||||
| 0 | 체크박스 | center | - |
|
||||
| 1 | 번호 | center | globalIndex + 1 |
|
||||
| 2 | 구분 | center | Badge (매출/매입/매입매출) |
|
||||
| 3 | 거래처명 | left | - |
|
||||
| 4 | 매입 결제일 | center | {n}일 |
|
||||
| 5 | 매출 결제일 | center | {n}일 |
|
||||
| 6 | 신용등급 | center | Badge |
|
||||
| 7 | 거래등급 | center | Badge |
|
||||
| 8 | 미수금 | right | 금액 포맷 |
|
||||
| 9 | 악성채권 | center | Badge or - |
|
||||
|
||||
#### 2.2.4 행 선택 시 버튼
|
||||
- [x] 상세 버튼 → `/accounting/vendors/{id}` 이동
|
||||
- [x] 수정 버튼 → `/accounting/vendors/{id}?mode=edit` 이동
|
||||
- [x] 삭제 버튼 → 삭제 확인 AlertDialog
|
||||
- [x] 취소 버튼 → 선택 해제
|
||||
|
||||
#### 2.2.5 헤더 액션
|
||||
- [x] 신규등록 버튼 → `/accounting/vendors/new` 이동
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: 상세/수정/등록 페이지 구현 ✅
|
||||
|
||||
### 3.1 페이지 라우트 생성
|
||||
- [x] `/src/app/[locale]/(protected)/accounting/vendors/[id]/page.tsx` (상세/수정)
|
||||
- [x] `/src/app/[locale]/(protected)/accounting/vendors/new/page.tsx` (신규등록)
|
||||
|
||||
### 3.2 VendorDetail 컴포넌트
|
||||
- [x] `/src/components/accounting/VendorManagement/VendorDetail.tsx` 생성
|
||||
- [x] mode prop: `view` | `edit` | `new`
|
||||
|
||||
#### 3.2.1 상단 버튼
|
||||
| 모드 | 버튼 |
|
||||
|------|------|
|
||||
| view | 삭제, 수정 |
|
||||
| edit | 취소, 저장 |
|
||||
| new | 취소, 등록 |
|
||||
|
||||
#### 3.2.2 기본 정보 섹션
|
||||
| 필드명 | 타입 | 필수 | 비고 |
|
||||
|--------|------|------|------|
|
||||
| 사업자등록번호 | text (마스크: 000-00-00000) | * | - |
|
||||
| 거래처코드 | text | - | 자동생성 또는 수동입력 |
|
||||
| 거래처명 | text | * | - |
|
||||
| 대표자명 | text | - | - |
|
||||
| 거래처 유형 | select | * | 매출매입, 매출, 매입 |
|
||||
| 업태 | text | - | - |
|
||||
| 업종 | text | - | - |
|
||||
|
||||
#### 3.2.3 연락처 정보 섹션
|
||||
| 필드명 | 타입 | 필수 | 비고 |
|
||||
|--------|------|------|------|
|
||||
| 주소 | address | - | 우편번호 찾기 + 기본주소 + 상세주소 |
|
||||
| 전화번호 | tel | - | 02-0000-0000 |
|
||||
| 모바일 | tel | - | 010-0000-0000 |
|
||||
| 팩스 | tel | - | 02-0000-0000 |
|
||||
| 이메일 | email | - | - |
|
||||
|
||||
#### 3.2.4 담당자 정보 섹션
|
||||
| 필드명 | 타입 | 필수 | 비고 |
|
||||
|--------|------|------|------|
|
||||
| 담당자명 | text | - | - |
|
||||
| 담당자 전화 | tel | - | - |
|
||||
| 시스템 관리자 | text | - | - |
|
||||
|
||||
#### 3.2.5 회사 정보 섹션
|
||||
| 필드명 | 타입 | 필수 | 비고 |
|
||||
|--------|------|------|------|
|
||||
| 회사 로고 | file | - | 750x250px, 10MB 이하, PNG/JPEG/GIF |
|
||||
| 매입 결제일 | select | - | 1~31일 |
|
||||
| 매출 결제일 | select | - | 1~31일 |
|
||||
|
||||
#### 3.2.6 신용/거래 정보 섹션
|
||||
| 필드명 | 타입 | 필수 | 비고 |
|
||||
|--------|------|------|------|
|
||||
| 신용등급 | select | - | AAA~D |
|
||||
| 거래등급 | select | - | A(우수)~E(위험) |
|
||||
| 세금계산서 이메일 | email | - | - |
|
||||
| 입금계좌 은행 | select | - | 은행 목록 |
|
||||
| 계좌 | text | - | 숫자만 |
|
||||
| 예금주 | text | - | - |
|
||||
|
||||
#### 3.2.7 추가 정보 섹션
|
||||
| 필드명 | 타입 | 필수 | 비고 |
|
||||
|--------|------|------|------|
|
||||
| 미수금 | number | - | 금액 입력 |
|
||||
| 연체 | number + toggle | - | 일수 + ON/OFF |
|
||||
| 미지급 | number | - | 금액 입력 |
|
||||
| 악성채권 | select + toggle | - | 선택 + ON/OFF (악성채권 추심관리 연동) |
|
||||
|
||||
#### 3.2.8 메모 섹션
|
||||
| 필드명 | 타입 | 비고 |
|
||||
|--------|------|------|
|
||||
| 메모 | textarea + list | 추가 버튼으로 메모 리스트에 추가 |
|
||||
| 메모 리스트 | table | 날짜, 내용, 삭제버튼 |
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: 다이얼로그 ✅
|
||||
|
||||
### 4.1 삭제 확인 다이얼로그
|
||||
- [x] AlertDialog 사용
|
||||
- [x] 제목: "거래처(명)을 삭제하시겠습니까?"
|
||||
- [x] 설명: 확인 클릭 시 거래처관리 목록으로 이동
|
||||
- [x] 버튼: 취소, 삭제
|
||||
|
||||
### 4.2 수정 확인 다이얼로그
|
||||
- [x] AlertDialog 사용
|
||||
- [x] 제목: "정말 수정하시겠습니까?"
|
||||
- [x] 설명: 확인 클릭 시 "수정이 완료되었습니다." 알림
|
||||
- [x] 버튼: 취소, 확인
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: 파일 구조 ✅
|
||||
|
||||
```
|
||||
src/
|
||||
├── app/[locale]/(protected)/accounting/vendors/
|
||||
│ ├── page.tsx # 리스트 페이지 ✅
|
||||
│ ├── [id]/
|
||||
│ │ └── page.tsx # 상세/수정 페이지 ✅
|
||||
│ └── new/
|
||||
│ └── page.tsx # 신규등록 페이지 ✅
|
||||
└── components/accounting/VendorManagement/
|
||||
├── index.tsx # 리스트 컴포넌트 ✅
|
||||
├── VendorDetail.tsx # 상세/수정/등록 컴포넌트 ✅
|
||||
└── types.ts # 타입 및 상수 정의 ✅
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 구현 완료 요약
|
||||
|
||||
### Step 1: 기본 구조 ✅
|
||||
- [x] types.ts 생성 및 타입/상수 정의
|
||||
- [x] 페이지 라우트 파일 생성
|
||||
|
||||
### Step 2: 리스트 페이지 ✅
|
||||
- [x] index.tsx 생성
|
||||
- [x] IntegratedListTemplateV2 활용
|
||||
- [x] 통계 카드 (3개)
|
||||
- [x] 필터 (5개 셀렉트박스)
|
||||
- [x] 테이블 (체크박스 + 번호 + 9개 컬럼)
|
||||
- [x] 행 선택 시 버튼 (상세/수정/삭제/취소)
|
||||
- [x] 삭제 확인 다이얼로그
|
||||
|
||||
### Step 3: 상세 페이지 ✅
|
||||
- [x] VendorDetail.tsx 생성
|
||||
- [x] 기본 정보 섹션
|
||||
- [x] 연락처 정보 섹션
|
||||
- [x] 담당자 정보 섹션
|
||||
- [x] 회사 정보 섹션 (로고 업로드 영역 포함)
|
||||
- [x] 신용/거래 정보 섹션
|
||||
- [x] 추가 정보 섹션 (토글 포함)
|
||||
- [x] 메모 섹션
|
||||
|
||||
### Step 4: 수정/등록 기능 ✅
|
||||
- [x] mode별 버튼 및 동작 구현
|
||||
- [x] 수정/삭제 확인 다이얼로그
|
||||
|
||||
---
|
||||
|
||||
## 테스트 URL
|
||||
|
||||
| 페이지 | URL |
|
||||
|--------|-----|
|
||||
| 리스트 | `/ko/accounting/vendors` |
|
||||
| 상세 | `/ko/accounting/vendors/vendor-1` |
|
||||
| 수정 | `/ko/accounting/vendors/vendor-1?mode=edit` |
|
||||
| 신규등록 | `/ko/accounting/vendors/new` |
|
||||
|
||||
---
|
||||
|
||||
## 참고 사항
|
||||
|
||||
### 공통 컴포넌트 사용
|
||||
- IntegratedListTemplateV2 (리스트 템플릿)
|
||||
- AlertDialog (삭제/수정 확인)
|
||||
- Card, CardHeader, CardContent (섹션 구분)
|
||||
- Select, Input, Textarea (폼 요소)
|
||||
- Switch (토글)
|
||||
- Badge (상태 표시)
|
||||
|
||||
### 스타일 가이드
|
||||
- 주요 색상: orange (강조), blue/green (통계 카드)
|
||||
- Badge 색상:
|
||||
- 구분: 매출(green), 매입(orange), 매입매출(blue)
|
||||
- 신용등급: AAA~A(green), BBB~B(yellow), CCC~D(red)
|
||||
- 거래등급: A(green), B(blue), C(yellow), D(orange), E(red)
|
||||
- 악성채권: 정상(green), 미설정(-), 경고(red)
|
||||
|
||||
### 주의사항
|
||||
- 상세 페이지는 **페이지**로 구현 (모달 X) ✅
|
||||
- 테이블에 번호 컬럼 필수 (체크박스 바로 뒤) ✅
|
||||
- 금액은 toLocaleString() 포맷 사용 ✅
|
||||
- 행 클릭 시 상세 페이지로 이동 ✅
|
||||
|
||||
---
|
||||
|
||||
## 리스트 페이지 수정 (2025-12-18)
|
||||
|
||||
### 수정 사항
|
||||
- [x] 악성채권 필터 옵션: "미설정" → "악성채권" 변경
|
||||
- [x] BadDebtStatus 타입: 'warning' → 'badDebt' 변경
|
||||
- [x] 작업 버튼: 상단 액션바 → 테이블 맨 끝 컬럼으로 이동
|
||||
- [x] 체크박스 선택 시에만 수정/삭제 버튼 표시
|
||||
- [x] headerActions (신규등록 버튼) 제거
|
||||
- [x] beforeTableContent (상단 액션 바) 제거
|
||||
- [x] 미사용 핸들러 정리 (handleNewVendor, handleCancelSelection)
|
||||
@@ -0,0 +1,142 @@
|
||||
# 출금관리 (Withdrawal Management) 구현 체크리스트
|
||||
|
||||
> **상태**: ✅ 완료 (2025-12-18)
|
||||
> **참고**: 입금관리(DepositManagement)와 동일한 구조
|
||||
|
||||
## 개요
|
||||
- **경로**: `/accounting/withdrawals` (리스트), `/accounting/withdrawals/[id]` (상세)
|
||||
- **기능**: 출금 내역 조회, 수정 (계좌 관리에 등록된 계좌의 자동 출금 내역 수집)
|
||||
- **참고**: 스크린샷 3장
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: 타입 및 상수 정의
|
||||
|
||||
### 1.1 types.ts 생성
|
||||
- [ ] 출금 유형 타입 (WithdrawalType)
|
||||
- `unset` (미설정)
|
||||
- `purchasePayment` (매입대금)
|
||||
- `advance` (선급금)
|
||||
- `suspense` (가지급금)
|
||||
- `rent` (임대료)
|
||||
- `interestExpense` (이자비용)
|
||||
- `depositPayment` (보증금 지급)
|
||||
- `loanRepayment` (차입금 상환)
|
||||
- `dividendPayment` (배당금 지급)
|
||||
- `vatPayment` (부가세 납부)
|
||||
- `salary` (급여)
|
||||
- `insurance` (4대보험)
|
||||
- `tax` (세금)
|
||||
- `utilities` (공과금)
|
||||
- `expenses` (경비)
|
||||
- `other` (기타)
|
||||
|
||||
- [ ] 정렬 옵션 (SortOption): 최신순, 등록순, 금액 높은순, 금액 낮은순
|
||||
- [ ] 출금 레코드 인터페이스 (WithdrawalRecord)
|
||||
- id, withdrawalDate, withdrawalAmount
|
||||
- accountName, recipientName (수취인명)
|
||||
- note (적요), withdrawalType
|
||||
- vendorId, vendorName
|
||||
- createdAt, updatedAt
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: 리스트 페이지 구현
|
||||
|
||||
### 2.1 페이지 라우트
|
||||
- [ ] `/src/app/[locale]/(protected)/accounting/withdrawals/page.tsx`
|
||||
|
||||
### 2.2 WithdrawalManagement 컴포넌트
|
||||
- [ ] `/src/components/accounting/WithdrawalManagement/index.tsx`
|
||||
|
||||
#### 2.2.1 상단 통계 카드 (4개)
|
||||
| 카드 | 값 | 아이콘 색상 |
|
||||
|------|-----|------------|
|
||||
| 출금 총액 | {amount}원 | blue |
|
||||
| 당월 출금 | {amount}원 | green |
|
||||
| 거래처 미설정 | {count}건 | orange |
|
||||
| 출금유형 미설정 | {count}건 | red |
|
||||
|
||||
#### 2.2.2 헤더 액션
|
||||
- [ ] DateRangeSelector (날짜 범위)
|
||||
- [ ] 빠른 필터: 당해연도, 전년도, 전월, 당월, 어제, 오늘, 새로고침
|
||||
|
||||
#### 2.2.3 계정과목명 셀렉트 + 저장 버튼
|
||||
- [ ] 계정과목명 Select (출금 유형 옵션)
|
||||
- [ ] 저장 버튼 → "N개의 출금 유형을 {계정과목명}으로 모두 변경하시겠습니까?" Alert
|
||||
|
||||
#### 2.2.4 필터 (3개)
|
||||
| 필터 | 옵션 |
|
||||
|------|------|
|
||||
| 거래처 | 전체, 거래처 목록 |
|
||||
| 출금유형 | 전체, 매입대금, 선급금, 가지급금, 임대료, 이자비용, 보증금 지급, 차입금 상환, 배당금 지급, 부가세 납부, 급여, 4대보험, 세금, 공과금, 경비, 기타, 미설정 |
|
||||
| 정렬 | 최신순, 등록순, 금액 높은순, 금액 낮은순 |
|
||||
|
||||
#### 2.2.5 테이블 컬럼 (체크박스 + 7개 + 작업)
|
||||
| 순서 | 컬럼명 | 정렬 | 비고 |
|
||||
|------|--------|------|------|
|
||||
| 0 | 체크박스 | center | - |
|
||||
| 1 | 출금일 | center | - |
|
||||
| 2 | 출금계좌 | left | - |
|
||||
| 3 | 수취인명 | left | - |
|
||||
| 4 | 출금금액 | right | 금액 포맷 |
|
||||
| 5 | 거래처 | left | 미설정시 빨간색 |
|
||||
| 6 | 출금유형 | center | Badge |
|
||||
| 7 | 작업 | center | 선택 시 수정/삭제 버튼 |
|
||||
|
||||
#### 2.2.6 테이블 합계 행
|
||||
- [ ] 출금금액 합계
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: 상세 페이지 구현
|
||||
|
||||
### 3.1 페이지 라우트
|
||||
- [ ] `/src/app/[locale]/(protected)/accounting/withdrawals/[id]/page.tsx`
|
||||
|
||||
### 3.2 WithdrawalDetail 컴포넌트
|
||||
- [ ] `/src/components/accounting/WithdrawalManagement/WithdrawalDetail.tsx`
|
||||
- [ ] mode prop: `view` | `edit`
|
||||
|
||||
#### 3.2.1 상단 버튼
|
||||
| 모드 | 버튼 |
|
||||
|------|------|
|
||||
| view | 목록, 삭제, 수정 |
|
||||
| edit | 취소, 저장 |
|
||||
|
||||
#### 3.2.2 기본 정보 섹션
|
||||
| 필드명 | 타입 | 편집 가능 | 비고 |
|
||||
|--------|------|----------|------|
|
||||
| 출금일 | date | ❌ | 읽기 전용 |
|
||||
| 출금계좌 | text | ❌ | 읽기 전용 |
|
||||
| 수취인명 | text | ❌ | 읽기 전용 |
|
||||
| 출금금액 | number | ❌ | 읽기 전용 |
|
||||
| 적요 | select | ✅ | 선택 가능 |
|
||||
| 거래처 | select | ✅ | 필수 |
|
||||
| 출금 유형 | select | ✅ | 필수 |
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: 파일 구조
|
||||
|
||||
```
|
||||
src/
|
||||
├── app/[locale]/(protected)/accounting/withdrawals/
|
||||
│ ├── page.tsx # 리스트 페이지
|
||||
│ └── [id]/
|
||||
│ └── page.tsx # 상세/수정 페이지
|
||||
└── components/accounting/WithdrawalManagement/
|
||||
├── index.tsx # 리스트 컴포넌트
|
||||
├── WithdrawalDetail.tsx # 상세/수정 컴포넌트
|
||||
└── types.ts # 타입 및 상수 정의
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 테스트 URL
|
||||
|
||||
| 페이지 | URL |
|
||||
|--------|-----|
|
||||
| 리스트 | `/ko/accounting/withdrawals` |
|
||||
| 상세 | `/ko/accounting/withdrawals/withdrawal-1` |
|
||||
| 수정 | `/ko/accounting/withdrawals/withdrawal-1?mode=edit` |
|
||||
@@ -0,0 +1,230 @@
|
||||
# 악성채권 추심관리 구현 계획서
|
||||
|
||||
> 작성일: 2025-12-19
|
||||
> URL: `/ko/accounting/bad-debt-collection`
|
||||
|
||||
---
|
||||
|
||||
## 📋 스크린샷 분석 요약
|
||||
|
||||
### 리스트 화면
|
||||
- **제목**: 악성채권 추심관리
|
||||
- **통계 카드 4개**: 총 악성채권, 추심중, 법적조치, 회수완료
|
||||
- **필터 3개**:
|
||||
1. 거래처 필터 (검색 + 다중선택, 디폴트: 전체)
|
||||
2. 상태 필터 (전체, 추심중, 법적조치, 회수완료, 대손처리)
|
||||
3. 정렬 (최신순, 등록순, 디폴트: 최신순)
|
||||
- **테이블 컬럼**: No., 거래처, 채권금액, 발생일, 연체일수, 담당자, 상태, 설정, 작업
|
||||
- **설정 컬럼**: ON/OFF 토글
|
||||
- **작업 컬럼**: 수정/삭제 아이콘
|
||||
|
||||
### 상세 화면 (view/edit/new 모드)
|
||||
1. **기본 정보**: 사업자등록번호, 거래처코드, 거래처명, 대표자명, 거래처유형(토글), 악성채권등록(업태/업종)
|
||||
2. **연락처 정보**: 주소(우편번호 찾기), 전화번호, 모바일, 팩스, 이메일
|
||||
3. **담당자 정보**: 담당자명, 담당자전화, 시스템관리자
|
||||
4. **필요 서류**: 사업자등록증, 세금계산서, 추가서류 (파일 첨부)
|
||||
5. **악성 채권 정보**:
|
||||
- 미수금 + 상태 셀렉트 (추심중, 법적조치, 회수완료, 대손처리)
|
||||
- 연체일수 + 본사 담당자 셀렉트 (부서명 이름 직급명 연락처)
|
||||
- 악성채권 발생일 / 종료일
|
||||
- **수취 어음 현황 버튼** → 어음관리 화면 (해당 거래처 필터)
|
||||
- **거래처 미수금 현황 버튼** → 미수금 현황 화면 (해당 거래처 하이라이트)
|
||||
6. **메모**: 타임스탬프 형식 메모 목록
|
||||
|
||||
---
|
||||
|
||||
## ✅ 구현 체크리스트
|
||||
|
||||
### Phase 1: 파일 구조 및 타입 정의
|
||||
- [x] 1.1 `src/components/accounting/BadDebtCollection/types.ts` 생성
|
||||
- BadDebtRecord 인터페이스
|
||||
- CollectionStatus 타입 (추심중, 법적조치, 회수완료, 대손처리)
|
||||
- SortOption 타입
|
||||
- 상태 라벨/컬러 상수
|
||||
- [x] 1.2 `src/app/[locale]/(protected)/accounting/bad-debt-collection/page.tsx` 생성
|
||||
- [x] 1.3 `src/app/[locale]/(protected)/accounting/bad-debt-collection/[id]/page.tsx` 생성
|
||||
- [x] 1.4 `src/app/[locale]/(protected)/accounting/bad-debt-collection/[id]/edit/page.tsx` 생성
|
||||
- [x] 1.5 `src/app/[locale]/(protected)/accounting/bad-debt-collection/new/page.tsx` 생성
|
||||
|
||||
### Phase 2: 리스트 컴포넌트 구현
|
||||
- [x] 2.1 `src/components/accounting/BadDebtCollection/index.tsx` 생성
|
||||
- IntegratedListTemplateV2 사용
|
||||
- [x] 2.2 통계 카드 4개 구현 (총 악성채권, 추심중, 법적조치, 회수완료)
|
||||
- [x] 2.3 필터 3개 구현
|
||||
- 거래처 필터 (검색 + 다중선택)
|
||||
- 상태 필터 (전체, 추심중, 법적조치, 회수완료, 대손처리)
|
||||
- 정렬 (최신순, 등록순)
|
||||
- [x] 2.4 테이블 컬럼 구현 (체크박스 + No. + 거래처 + 채권금액 + 발생일 + 연체일수 + 담당자 + 상태 + 설정 + 작업)
|
||||
- No.: 순번 (1부터)
|
||||
- 거래처: 거래처명
|
||||
- 채권금액: 금액 (원)
|
||||
- 발생일: YYYY-MM-DD
|
||||
- 연체일수: 숫자 + "일"
|
||||
- 담당자: 담당자명
|
||||
- 상태: Badge (추심중/법적조치/회수완료/대손처리)
|
||||
- 설정: ON/OFF 토글 (Switch)
|
||||
- 작업: 수정/삭제 아이콘 (row 선택 시 표시)
|
||||
- [x] 2.5 Mock 데이터 생성 함수
|
||||
- [x] 2.6 행 클릭 → 상세 페이지 이동 기능
|
||||
- [x] 2.7 모바일 카드 렌더링
|
||||
|
||||
### Phase 3: 상세/수정/등록 컴포넌트 구현
|
||||
- [x] 3.1 `src/components/accounting/BadDebtCollection/BadDebtDetail.tsx` 생성
|
||||
- mode prop (view/edit/new)
|
||||
- [x] 3.2 기본 정보 섹션
|
||||
- 사업자등록번호 (Input, readonly)
|
||||
- 거래처 코드 (Input, readonly)
|
||||
- 거래처명 (Input)
|
||||
- 대표자명 (Input)
|
||||
- 거래처 유형 (Switch - 매출매입)
|
||||
- 악성채권 등록 - 업태 (Input)
|
||||
- 악성채권 등록 - 업종 (Input)
|
||||
- [x] 3.3 연락처 정보 섹션
|
||||
- 주소 (우편번호 찾기 버튼 + Input 3개)
|
||||
- 전화번호 (Input)
|
||||
- 모바일 (Input)
|
||||
- 팩스 (Input)
|
||||
- 이메일 (Input)
|
||||
- [x] 3.4 담당자 정보 섹션
|
||||
- 담당자명 (Input)
|
||||
- 담당자 전화 (Input)
|
||||
- 시스템 관리자 (Input, readonly)
|
||||
- [x] 3.5 필요 서류 섹션
|
||||
- 사업자등록증 (파일 찾기)
|
||||
- 세금계산서 (파일 찾기)
|
||||
- 추가 서류 (파일 찾기 + 추가 버튼 + 삭제 X)
|
||||
- [x] 3.6 악성 채권 정보 섹션
|
||||
- 미수금 (Input, readonly)
|
||||
- 상태 (Select: 추심중, 법적조치, 회수완료, 대손처리)
|
||||
- 연체일수 (Switch + Input)
|
||||
- 본사 담당자 (Select: 부서명 이름 직급명 연락처)
|
||||
- 악성채권 발생일 (DatePicker)
|
||||
- 악성채권 종료일 (DatePicker)
|
||||
- **수취 어음 현황 버튼** → `/ko/accounting/bills?vendorId={id}&type=received`
|
||||
- **거래처 미수금 현황 버튼** → `/ko/accounting/receivables-status?highlight={id}`
|
||||
- [x] 3.7 메모 섹션
|
||||
- 메모 목록 (Textarea, readonly)
|
||||
- 메모 추가 기능 (타임스탬프 자동 생성)
|
||||
- [x] 3.8 삭제/수정 버튼 및 다이얼로그
|
||||
- [x] 3.9 저장 기능 (mock)
|
||||
|
||||
### Phase 4: 연동 기능 구현
|
||||
- [x] 4.1 어음관리 페이지 수정 - vendorId 쿼리 파라미터 지원
|
||||
- [x] 4.2 미수금 현황 페이지 수정 - highlight 쿼리 파라미터 지원 (해당 거래처 행 하이라이트)
|
||||
|
||||
### Phase 5: 최종 검증 및 문서화
|
||||
- [x] 5.1 리스트 페이지 테스트
|
||||
- [x] 5.2 상세/수정/등록 페이지 테스트
|
||||
- [x] 5.3 어음관리 연동 테스트
|
||||
- [x] 5.4 미수금 현황 연동 테스트
|
||||
- [x] 5.5 모바일 반응형 테스트
|
||||
- [x] 5.6 `[REF] all-pages-test-urls.md` 업데이트
|
||||
|
||||
---
|
||||
|
||||
## 📁 파일 구조
|
||||
|
||||
```
|
||||
src/
|
||||
├── app/[locale]/(protected)/accounting/
|
||||
│ └── bad-debt-collection/
|
||||
│ ├── page.tsx # 리스트 페이지
|
||||
│ ├── new/
|
||||
│ │ └── page.tsx # 등록 페이지
|
||||
│ └── [id]/
|
||||
│ ├── page.tsx # 상세 페이지
|
||||
│ └── edit/
|
||||
│ └── page.tsx # 수정 페이지
|
||||
└── components/accounting/
|
||||
└── BadDebtCollection/
|
||||
├── index.tsx # 리스트 컴포넌트
|
||||
├── BadDebtDetail.tsx # 상세/수정/등록 컴포넌트
|
||||
└── types.ts # 타입 정의
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔗 URL 구조
|
||||
|
||||
| 페이지 | URL | 비고 |
|
||||
|--------|-----|------|
|
||||
| 리스트 | `/ko/accounting/bad-debt-collection` | 메인 |
|
||||
| 등록 | `/ko/accounting/bad-debt-collection/new` | |
|
||||
| 상세 | `/ko/accounting/bad-debt-collection/[id]` | |
|
||||
| 수정 | `/ko/accounting/bad-debt-collection/[id]/edit` | |
|
||||
|
||||
---
|
||||
|
||||
## 📊 테이블 컬럼 상세
|
||||
|
||||
| 컬럼 | key | label | 정렬 | 비고 |
|
||||
|------|-----|-------|------|------|
|
||||
| 체크박스 | checkbox | - | center | Checkbox |
|
||||
| 번호 | no | No. | center | 1부터 시작 |
|
||||
| 거래처 | vendorName | 거래처 | left | |
|
||||
| 채권금액 | debtAmount | 채권금액 | right | 1,000,000원 |
|
||||
| 발생일 | occurrenceDate | 발생일 | center | YYYY-MM-DD |
|
||||
| 연체일수 | overdueDays | 연체일수 | center | 100일 |
|
||||
| 담당자 | managerName | 담당자 | left | |
|
||||
| 상태 | status | 상태 | center | Badge |
|
||||
| 설정 | settingToggle | 설정 | center | Switch |
|
||||
| 작업 | actions | 작업 | center | 수정/삭제 |
|
||||
|
||||
---
|
||||
|
||||
## 🎨 상태 Badge 스타일
|
||||
|
||||
| 상태 | 라벨 | 스타일 |
|
||||
|------|------|--------|
|
||||
| collecting | 추심중 | `border-orange-300 text-orange-600 bg-orange-50` |
|
||||
| legalAction | 법적조치 | `border-red-300 text-red-600 bg-red-50` |
|
||||
| recovered | 회수완료 | `border-green-300 text-green-600 bg-green-50` |
|
||||
| badDebt | 대손처리 | `border-gray-300 text-gray-600 bg-gray-50` |
|
||||
|
||||
---
|
||||
|
||||
## 🔍 필터 상세
|
||||
|
||||
### 1. 거래처 필터
|
||||
- **타입**: 검색 + 다중선택 (Combobox/MultiSelect)
|
||||
- **옵션**: 전체, 거래처 목록 (API)
|
||||
- **디폴트**: 전체
|
||||
|
||||
### 2. 상태 필터
|
||||
- **타입**: Select
|
||||
- **옵션**: 전체, 추심중, 법적조치, 회수완료, 대손처리
|
||||
- **디폴트**: 악성채권 설정 시 디폴트 상태
|
||||
|
||||
### 3. 정렬
|
||||
- **타입**: Select
|
||||
- **옵션**: 최신순, 등록순
|
||||
- **디폴트**: 최신순
|
||||
|
||||
---
|
||||
|
||||
## 🔗 연동 기능
|
||||
|
||||
### 수취 어음 현황 버튼
|
||||
- **대상**: 어음관리 페이지 (`/ko/accounting/bills`)
|
||||
- **쿼리**: `?vendorId={id}&type=received`
|
||||
- **동작**: 해당 거래처의 수취 어음만 필터링하여 표시
|
||||
|
||||
### 거래처 미수금 현황 버튼
|
||||
- **대상**: 미수금 현황 페이지 (`/ko/accounting/receivables-status`)
|
||||
- **쿼리**: `?highlight={id}`
|
||||
- **동작**: 해당 거래처 행을 하이라이트 표시
|
||||
|
||||
---
|
||||
|
||||
## 📝 작업 완료 후 체크
|
||||
|
||||
- [x] `claudedocs/[REF] all-pages-test-urls.md` 업데이트
|
||||
- [x] 빌드 오류 없음 확인
|
||||
- [x] 리스트/상세/수정/등록 페이지 정상 작동
|
||||
- [x] 연동 페이지 정상 작동
|
||||
|
||||
---
|
||||
|
||||
## ✅ 구현 완료
|
||||
|
||||
> 완료일: 2025-12-19
|
||||
@@ -0,0 +1,89 @@
|
||||
# 카드 내역 조회 구현 체크리스트
|
||||
|
||||
## 개요
|
||||
- **페이지 경로**: `/ko/accounting/card-transactions`
|
||||
- **참고**: 입출금 계좌조회 페이지와 유사한 구조
|
||||
|
||||
## 화면 구성
|
||||
|
||||
### 1. 상단 영역
|
||||
- 제목: "카드 내역 조회"
|
||||
- 부제: "법인카드 사용 내역을 조회합니다"
|
||||
|
||||
### 2. 날짜 선택 + 빠른 버튼
|
||||
- 날짜 범위 선택
|
||||
- 빠른 버튼: 당해년도, 전전월, 전월, 당월, 어제, 오늘
|
||||
- 새로고침 버튼 (출금관리 스타일, 오른쪽 위치)
|
||||
|
||||
### 3. 요약 카드 (2개)
|
||||
- 전월 사용액: 금액 표시
|
||||
- 당월 사용액: 금액 표시
|
||||
|
||||
### 4. 필터 영역
|
||||
- 검색 입력창 (좌측)
|
||||
- 총 N건 표시
|
||||
- 필터 2개:
|
||||
- 카드명 필터 (전체, 카드명 목록) - 디폴트: 전체
|
||||
- 정렬 필터 (최신순, 등록순, 금액 높은순, 금액 낮은순) - 디폴트: 최신순
|
||||
|
||||
### 5. 테이블
|
||||
- **체크박스 없음**
|
||||
- **번호 컬럼 없음**
|
||||
- 컬럼 순서:
|
||||
1. 카드
|
||||
2. 카드명
|
||||
3. 사용자
|
||||
4. 사용일시
|
||||
5. 가맹점명
|
||||
6. 사용금액
|
||||
|
||||
### 6. 합계 행
|
||||
- 테이블 마지막에 합계 행
|
||||
- 사용금액 열에만 합계 표시
|
||||
|
||||
---
|
||||
|
||||
## 구현 체크리스트
|
||||
|
||||
### Phase 1: 파일 구조 생성
|
||||
- [x] types.ts 생성 (타입 정의)
|
||||
- [x] index.tsx 생성 (메인 컴포넌트)
|
||||
- [x] page.tsx 생성 (라우트)
|
||||
|
||||
### Phase 2: 타입 정의
|
||||
- [x] CardTransaction 인터페이스
|
||||
- [x] SortOption 타입
|
||||
|
||||
### Phase 3: 컴포넌트 구현
|
||||
- [x] 헤더 영역 (제목, 부제)
|
||||
- [x] 날짜 선택 + 빠른 버튼
|
||||
- [x] 새로고침 버튼
|
||||
- [x] 요약 카드 2개 (전월/당월 사용액)
|
||||
- [x] 검색 + 필터 영역 (카드명, 정렬)
|
||||
- [x] 테이블 (체크박스/번호 없음)
|
||||
- [x] 합계 행
|
||||
|
||||
### Phase 4: 테스트 및 문서화
|
||||
- [x] all-pages-test-urls.md 업데이트
|
||||
|
||||
### Phase 5: 템플릿 기능 추가
|
||||
- [x] IntegratedListTemplateV2에 showCheckbox props 추가
|
||||
- [x] 조건부 체크박스 렌더링 구현
|
||||
|
||||
---
|
||||
|
||||
## Mock 데이터 예시
|
||||
```typescript
|
||||
{
|
||||
card: '신한 1234',
|
||||
cardName: '법인카드1',
|
||||
user: '홍길동',
|
||||
usedAt: '2025-12-12 12:12',
|
||||
merchantName: '가맹점명',
|
||||
amount: 100000
|
||||
}
|
||||
```
|
||||
|
||||
## 참고 파일
|
||||
- `src/components/accounting/BankTransactionInquiry/index.tsx`
|
||||
- `src/components/accounting/BankTransactionInquiry/types.ts`
|
||||
270
claudedocs/accounting/[PLAN-2025-12-18] sales-management.md
Normal file
270
claudedocs/accounting/[PLAN-2025-12-18] sales-management.md
Normal file
@@ -0,0 +1,270 @@
|
||||
# [PLAN-2025-12-18] 매출관리 페이지 구현 계획서
|
||||
|
||||
## 개요
|
||||
- **목표**: 회계관리 > 매출관리 페이지 구현
|
||||
- **참조**: 기안함(DraftBox) 구조 기반
|
||||
- **페이지 수**: 2개 (리스트 + 상세/등록)
|
||||
|
||||
---
|
||||
|
||||
## 1. 리스트 페이지 (매출관리)
|
||||
|
||||
### 1.1 페이지 구조
|
||||
- [ ] 경로: `/accounting/sales`
|
||||
- [ ] 컴포넌트: `src/components/accounting/SalesManagement/index.tsx`
|
||||
- [ ] IntegratedListTemplateV2 사용
|
||||
|
||||
### 1.2 헤더 영역
|
||||
- [ ] DateRangeSelector (공통 달력)
|
||||
- [ ] 상태 필터 버튼들
|
||||
- [ ] 당월마감
|
||||
- [ ] 전월
|
||||
- [ ] 합의
|
||||
- [ ] 미수
|
||||
- [ ] 전체
|
||||
- [ ] 매출 등록 버튼 (클릭 시 등록 페이지로 이동)
|
||||
|
||||
### 1.3 통계 카드 (4개)
|
||||
- [ ] 매출금액 합계 (예: 3,123,000원)
|
||||
- [ ] 입금금액 합계 (예: 3,123,000원)
|
||||
- [ ] 미수건수 (예: 3건)
|
||||
- [ ] 전체건수 (예: 4건)
|
||||
|
||||
### 1.4 필터 셀렉트 박스들
|
||||
| 필터명 | 설명 | 옵션 | 기본값 |
|
||||
|--------|------|------|--------|
|
||||
| 계정과목별 | 계정과목명으로 분류 | 전체, 외상 매출, 상품 매출, 부품 매출, 공사 매출, 임대 수익, 기타 매출 | 제품 매출 |
|
||||
| 거래처 필터 | 거래처별 검색/필터 | 거래처 검색 (검색 가능) | - |
|
||||
| 매출유형 필터 | 다중 선택 가능 | 외상 매출, 상품 매출, 부품 매출, 공사 매출, 임대 수익, 기타 매출 | 전체 |
|
||||
| 정렬 | 단독 선택 | 최신순, 금액 높은 순, 금액 낮은 순 | 최신순 |
|
||||
|
||||
### 1.5 테이블 컬럼
|
||||
| 컬럼 | 설명 |
|
||||
|------|------|
|
||||
| 체크박스 | 다중 선택 |
|
||||
| 번호 | 순번 |
|
||||
| 매출번호 | 자동 채번 (포맷: 조합+자동생성) |
|
||||
| 매출일 | 날짜 |
|
||||
| 거래처명 | 거래처 |
|
||||
| 결제대면(?) | 결제 방식 |
|
||||
| 매출금액 | 금액 |
|
||||
| 미수금액 | 미수 금액 |
|
||||
| 비고 | 적요 |
|
||||
| 작업 | 수정/삭제 아이콘 |
|
||||
|
||||
### 1.6 기능
|
||||
- [ ] 검색 기능 (매출번호, 거래처명, 적요)
|
||||
- [ ] 페이지네이션 (20건씩)
|
||||
- [ ] 체크박스 전체/개별 선택
|
||||
- [ ] 행 클릭 → 상세 페이지 이동
|
||||
|
||||
---
|
||||
|
||||
## 2. 상세/등록 페이지 (매출 상세)
|
||||
|
||||
### 2.1 페이지 구조
|
||||
- [ ] 경로: `/accounting/sales/[id]` (상세/수정)
|
||||
- [ ] 경로: `/accounting/sales/new` (신규 등록)
|
||||
- [ ] 컴포넌트: `src/components/accounting/SalesManagement/SalesDetail.tsx`
|
||||
|
||||
### 2.2 헤더 영역
|
||||
- [ ] 페이지 제목: "매출 상세" 또는 "매출 상세_직접 등록"
|
||||
- [ ] 버튼
|
||||
- [ ] 삭제 버튼 (신규 등록 시)
|
||||
- [ ] 수정 버튼
|
||||
|
||||
### 2.3 기본 정보 섹션
|
||||
| 필드 | 타입 | 설명 |
|
||||
|------|------|------|
|
||||
| 매출번호 | 텍스트 (읽기전용/자동채번) | 수정 시 표시, 신규 시 자동 채번 |
|
||||
| 매출일 | DatePicker | 날짜 선택 |
|
||||
| 거래처명 | Select (검색 가능) | 거래처 선택 |
|
||||
| 매출 유형 | Select | 외상 매출, 상품 매출, 부품 매출, 공사 매출, 임대 수익, 기타 매출 (기본: 제품 매출) |
|
||||
|
||||
### 2.4 품목 정보 섹션
|
||||
- [ ] 테이블 형태의 품목 입력
|
||||
| 컬럼 | 타입 | 설명 |
|
||||
|------|------|------|
|
||||
| 번호 | 자동 | 순번 |
|
||||
| 품목명 | Select/Input | 품목 선택 또는 입력 |
|
||||
| 수량 | Number | 수량 입력 |
|
||||
| 단가 | Number | 단가 입력 |
|
||||
| 공급가액 | Number (계산) | 수량 × 단가 자동 계산 |
|
||||
| 부가세 | Number (계산) | 공급가액 × 10% 자동 계산 |
|
||||
| 적요 | Text | 메모 |
|
||||
| 삭제 | Button | 행 삭제 (X 버튼) |
|
||||
|
||||
- [ ] 합계 행: 공급가액 합계, 부가세 합계
|
||||
- [ ] 추가 버튼: 품목 행 추가
|
||||
|
||||
### 2.5 세금계산서 섹션
|
||||
- [ ] 세금계산서 발행 토글 (Switch)
|
||||
- 클릭 시: 미발행 ↔ 발행완료 토글
|
||||
- 세금계산서 수동 발행 후 발행 상태로 변경
|
||||
|
||||
### 2.6 거래명세서 섹션
|
||||
- [ ] 거래명세서 발행 토글 (Switch)
|
||||
- 클릭 시: 미발행 ↔ 발행완료 토글
|
||||
- [ ] 거래명세서 발행하기 버튼
|
||||
- 클릭 시: 거래처 이메일로 자동 발송
|
||||
- Alert: "거래명세서가 'abc@email.com'으로 발송되었습니다"
|
||||
- 발행 후 자동으로 발행 상태 변경
|
||||
|
||||
---
|
||||
|
||||
## 3. 파일 구조
|
||||
|
||||
```
|
||||
src/
|
||||
├── app/[locale]/(protected)/accounting/
|
||||
│ └── sales/
|
||||
│ ├── page.tsx # 리스트 페이지
|
||||
│ ├── [id]/
|
||||
│ │ └── page.tsx # 상세/수정 페이지
|
||||
│ └── new/
|
||||
│ └── page.tsx # 신규 등록 페이지
|
||||
│
|
||||
└── components/accounting/
|
||||
└── SalesManagement/
|
||||
├── index.tsx # 리스트 컴포넌트
|
||||
├── SalesDetail.tsx # 상세/등록 컴포넌트
|
||||
├── SalesItemTable.tsx # 품목 테이블 컴포넌트
|
||||
├── TaxInvoiceSection.tsx # 세금계산서 섹션
|
||||
├── TransactionStatementSection.tsx # 거래명세서 섹션
|
||||
└── types.ts # 타입 정의
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 타입 정의 (types.ts)
|
||||
|
||||
```typescript
|
||||
// 매출 레코드
|
||||
interface SalesRecord {
|
||||
id: string;
|
||||
salesNo: string; // 매출번호
|
||||
salesDate: string; // 매출일
|
||||
vendorId: string; // 거래처 ID
|
||||
vendorName: string; // 거래처명
|
||||
salesType: SalesType; // 매출 유형
|
||||
items: SalesItem[]; // 품목 목록
|
||||
totalSupplyAmount: number; // 공급가액 합계
|
||||
totalVat: number; // 부가세 합계
|
||||
totalAmount: number; // 총 금액
|
||||
receivedAmount: number; // 입금액
|
||||
outstandingAmount: number; // 미수금액
|
||||
taxInvoiceIssued: boolean; // 세금계산서 발행 여부
|
||||
transactionStatementIssued: boolean; // 거래명세서 발행 여부
|
||||
note: string; // 비고
|
||||
status: SalesStatus; // 상태
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
// 매출 유형
|
||||
type SalesType =
|
||||
| 'credit' // 외상 매출
|
||||
| 'product' // 제품 매출
|
||||
| 'goods' // 상품 매출
|
||||
| 'parts' // 부품 매출
|
||||
| 'construction'// 공사 매출
|
||||
| 'rental' // 임대 수익
|
||||
| 'other'; // 기타 매출
|
||||
|
||||
// 매출 품목
|
||||
interface SalesItem {
|
||||
id: string;
|
||||
itemName: string; // 품목명
|
||||
quantity: number; // 수량
|
||||
unitPrice: number; // 단가
|
||||
supplyAmount: number; // 공급가액
|
||||
vat: number; // 부가세
|
||||
note: string; // 적요
|
||||
}
|
||||
|
||||
// 매출 상태
|
||||
type SalesStatus =
|
||||
| 'monthlyClose' // 당월마감
|
||||
| 'lastMonth' // 전월
|
||||
| 'agreed' // 합의
|
||||
| 'outstanding' // 미수
|
||||
| 'all'; // 전체
|
||||
|
||||
// 필터 옵션
|
||||
type FilterOption = 'all' | 'monthlyClose' | 'lastMonth' | 'agreed' | 'outstanding';
|
||||
|
||||
// 정렬 옵션
|
||||
type SortOption = 'latest' | 'amountHigh' | 'amountLow';
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 구현 체크리스트
|
||||
|
||||
### Phase 1: 기본 구조 설정
|
||||
- [ ] 폴더 구조 생성
|
||||
- [ ] 라우트 페이지 생성 (page.tsx들)
|
||||
- [ ] types.ts 작성
|
||||
|
||||
### Phase 2: 리스트 페이지 구현
|
||||
- [ ] SalesManagement/index.tsx 생성
|
||||
- [ ] IntegratedListTemplateV2 연동
|
||||
- [ ] DateRangeSelector 연동
|
||||
- [ ] 상태 필터 버튼 구현
|
||||
- [ ] 통계 카드 구현
|
||||
- [ ] 필터 셀렉트 박스들 구현
|
||||
- [ ] 테이블 렌더링 구현
|
||||
- [ ] 모바일 카드 렌더링 구현
|
||||
- [ ] 페이지네이션 구현
|
||||
- [ ] 검색 기능 구현
|
||||
- [ ] Mock 데이터 생성
|
||||
|
||||
### Phase 3: 상세/등록 페이지 구현
|
||||
- [ ] SalesDetail.tsx 생성
|
||||
- [ ] 기본 정보 섹션 구현
|
||||
- [ ] SalesItemTable.tsx 구현 (품목 테이블)
|
||||
- [ ] 품목 행 추가/삭제
|
||||
- [ ] 자동 계산 (공급가액, 부가세)
|
||||
- [ ] 합계 행
|
||||
- [ ] TaxInvoiceSection.tsx 구현
|
||||
- [ ] TransactionStatementSection.tsx 구현
|
||||
- [ ] 폼 유효성 검사
|
||||
- [ ] 저장/수정 로직
|
||||
|
||||
### Phase 4: 연동 및 테스트
|
||||
- [ ] 리스트 ↔ 상세 페이지 연결
|
||||
- [ ] 신규 등록 플로우 확인
|
||||
- [ ] 수정 플로우 확인
|
||||
- [ ] 반응형 레이아웃 확인
|
||||
|
||||
---
|
||||
|
||||
## 6. 참고 사항
|
||||
|
||||
### 기안함(DraftBox)에서 참고할 패턴
|
||||
- IntegratedListTemplateV2 사용법
|
||||
- DateRangeSelector 연동
|
||||
- 필터/정렬 셀렉트 박스 패턴
|
||||
- 테이블/모바일 카드 렌더링
|
||||
- 체크박스 선택 관리
|
||||
- 페이지네이션
|
||||
|
||||
### 주의 사항
|
||||
1. 매출번호 자동 채번 로직 확인 필요 (API 연동 시)
|
||||
2. 거래처 목록 API 연동 필요
|
||||
3. 품목 목록 API 연동 필요
|
||||
4. 세금계산서/거래명세서 발행 API 연동 필요
|
||||
|
||||
---
|
||||
|
||||
## 7. 예상 작업 시간
|
||||
- Phase 1: 기본 구조 설정
|
||||
- Phase 2: 리스트 페이지 구현
|
||||
- Phase 3: 상세/등록 페이지 구현
|
||||
- Phase 4: 연동 및 테스트
|
||||
|
||||
---
|
||||
|
||||
**작성일**: 2025-12-18
|
||||
**작성자**: Claude
|
||||
**상태**: 계획 검토 대기
|
||||
@@ -0,0 +1,204 @@
|
||||
# [PLAN-2025-12-19] 입출금 계좌조회 페이지 구현 계획서
|
||||
|
||||
> **작성일**: 2025-12-19
|
||||
> **상태**: 📋 대기 (사용자 확인 필요)
|
||||
> **참조**: DepositManagement, IntegratedListTemplateV2
|
||||
|
||||
---
|
||||
|
||||
## 1. 페이지 개요
|
||||
|
||||
| 항목 | 내용 |
|
||||
|------|------|
|
||||
| **페이지명** | 입출금 계좌조회 |
|
||||
| **설명** | 은행 계좌 정보와 입출금 내역을 조회할 수 있습니다 |
|
||||
| **URL** | `/ko/accounting/bank-transactions` |
|
||||
| **아이콘** | `Building2` (은행) 또는 `Wallet` |
|
||||
|
||||
---
|
||||
|
||||
## 2. UI 구성 분석 (스크린샷 기준)
|
||||
|
||||
### 2.1 헤더 영역
|
||||
- [x] 페이지 타이틀: "입출금 계좌조회"
|
||||
- [x] 설명: "은행 계좌 정보와 입출금 내역을 조회할 수 있습니다"
|
||||
|
||||
### 2.2 필터 영역 (DateRangeSelector 확장)
|
||||
- [ ] 기간 선택: 시작일 ~ 종료일 (DateRangeSelector 컴포넌트)
|
||||
- [ ] 탭 버튼 그룹:
|
||||
- `전체(선택)` - 전체 입출금 내역
|
||||
- `입금/수입` - 입금만 필터
|
||||
- `출금` - 출금만 필터
|
||||
- `입금` - (중복인지 확인 필요, 스크린샷 확인)
|
||||
- `어제` - 어제 날짜 빠른 선택
|
||||
- `오늘` - 오늘 날짜 빠른 선택
|
||||
- `새로고침` - 데이터 새로고침 버튼
|
||||
|
||||
### 2.3 통계 카드 (4개)
|
||||
| 순서 | 라벨 | 값 예시 | 아이콘 색상 |
|
||||
|------|-----------|---------|-------------|
|
||||
| 1 | 입금 | 3,123,000원 | 🔵 blue |
|
||||
| 2 | 출금 | 3,123,000원 | 🔴 red |
|
||||
| 3 | 입금 유형 미설정 | 4건 | 🟢 green |
|
||||
| 4 | 출금 유형 미설정 | 4건 | 🟠 orange |
|
||||
|
||||
### 2.4 검색 영역
|
||||
- [ ] 검색창 (은행명, 계좌번호, 거래처, 비고 검색)
|
||||
|
||||
### 2.5 테이블 컬럼 (14개)
|
||||
| 순서 | 컬럼명 | key | 정렬 | 비고 |
|
||||
|------|--------|-----|------|------|
|
||||
| 1 | 체크박스 | checkbox | center | 선택용 |
|
||||
| 2 | 번호 | no | center | globalIndex |
|
||||
| 3 | 은행명 | bankName | left | |
|
||||
| 4 | 계좌명 | accountName | left | |
|
||||
| 5 | 거래일시 | transactionDate | center | |
|
||||
| 6 | 구분 | type | center | 입금/출금 Badge |
|
||||
| 7 | 적요 | note | left | |
|
||||
| 8 | 거래처 | vendorName | left | |
|
||||
| 9 | 입금자/수취인 | depositorName | left | |
|
||||
| 10 | 입금 | depositAmount | right | 숫자 포맷 |
|
||||
| 11 | 출금 | withdrawalAmount | right | 숫자 포맷 |
|
||||
| 12 | 잔액 | balance | right | 숫자 포맷 |
|
||||
| 13 | 입출금 유형 | transactionType | center | Badge |
|
||||
| 14 | 작업 | actions | center | 수정 버튼 (체크 시 표시) |
|
||||
|
||||
### 2.6 테이블 합계 행
|
||||
- [ ] 합계 행 표시 (입금액 합계, 출금액 합계)
|
||||
|
||||
### 2.7 수정 버튼 동작
|
||||
- [ ] 수정 버튼 클릭 시:
|
||||
- 입금 데이터 → `/ko/accounting/deposits/{id}?mode=edit` 이동
|
||||
- 출금 데이터 → `/ko/accounting/withdrawals/{id}?mode=edit` 이동
|
||||
|
||||
---
|
||||
|
||||
## 3. 구현 체크리스트
|
||||
|
||||
### Phase 1: 기본 구조 설정
|
||||
- [ ] 1.1 페이지 라우트 생성 (`/accounting/bank-transactions/page.tsx`)
|
||||
- [ ] 1.2 컴포넌트 폴더 생성 (`/components/accounting/BankTransactionInquiry/`)
|
||||
- [ ] 1.3 types.ts 작성 (타입 정의)
|
||||
- [ ] 1.4 index.tsx 기본 구조 작성
|
||||
|
||||
### Phase 2: 타입 정의
|
||||
- [ ] 2.1 `BankTransaction` 인터페이스 정의
|
||||
```typescript
|
||||
interface BankTransaction {
|
||||
id: string;
|
||||
bankName: string; // 은행명
|
||||
accountName: string; // 계좌명
|
||||
transactionDate: string; // 거래일시
|
||||
type: 'deposit' | 'withdrawal'; // 구분 (입금/출금)
|
||||
note?: string; // 적요
|
||||
vendorId?: string; // 거래처 ID
|
||||
vendorName?: string; // 거래처명
|
||||
depositorName?: string; // 입금자/수취인
|
||||
depositAmount: number; // 입금
|
||||
withdrawalAmount: number; // 출금
|
||||
balance: number; // 잔액
|
||||
transactionType?: string; // 입출금 유형
|
||||
sourceId: string; // 원본 입금/출금 ID (상세 이동용)
|
||||
}
|
||||
```
|
||||
- [ ] 2.2 필터 타입 정의
|
||||
- [ ] 2.3 정렬 옵션 정의
|
||||
|
||||
### Phase 3: 통계 카드 구현
|
||||
- [ ] 3.1 총 입금 카드
|
||||
- [ ] 3.2 총 출금 카드
|
||||
- [ ] 3.3 입금 건 카드
|
||||
- [ ] 3.4 출금 건 카드
|
||||
|
||||
### Phase 4: 필터 영역 구현
|
||||
- [ ] 4.1 DateRangeSelector 연동
|
||||
- [ ] 4.2 탭 버튼 그룹 구현 (전체/입금/수입/출금/어제/오늘)
|
||||
- [ ] 4.3 새로고침 버튼
|
||||
- [ ] 4.4 빠른 날짜 선택 (어제/오늘) 로직
|
||||
|
||||
### Phase 5: 테이블 구현
|
||||
- [ ] 5.1 IntegratedListTemplateV2 연동
|
||||
- [ ] 5.2 테이블 컬럼 정의 (14개 컬럼)
|
||||
- [ ] 5.3 체크박스 기능
|
||||
- [ ] 5.4 번호 컬럼 (globalIndex 사용)
|
||||
- [ ] 5.5 구분 컬럼 Badge (입금: 파랑, 출금: 빨강)
|
||||
- [ ] 5.6 금액 컬럼 숫자 포맷팅
|
||||
- [ ] 5.7 합계 행 구현 (tableFooter)
|
||||
- [ ] 5.8 작업 컬럼 (체크 시 수정 버튼 표시)
|
||||
|
||||
### Phase 6: 상세 이동 로직
|
||||
- [ ] 6.1 수정 버튼 클릭 핸들러
|
||||
- [ ] 6.2 type에 따른 분기 처리
|
||||
- deposit → `/ko/accounting/deposits/{sourceId}?mode=edit`
|
||||
- withdrawal → `/ko/accounting/withdrawals/{sourceId}?mode=edit`
|
||||
|
||||
### Phase 7: Mock 데이터 및 테스트
|
||||
- [ ] 7.1 Mock 데이터 생성 함수
|
||||
- [ ] 7.2 필터링 로직 테스트
|
||||
- [ ] 7.3 정렬 로직 테스트
|
||||
- [ ] 7.4 페이지네이션 테스트
|
||||
|
||||
### Phase 8: 마무리
|
||||
- [ ] 8.1 모바일 카드 뷰 구현
|
||||
- [ ] 8.2 코드 정리 및 최적화
|
||||
- [ ] 8.3 테스트 URL 문서 업데이트 (`[REF] all-pages-test-urls.md`)
|
||||
|
||||
---
|
||||
|
||||
## 4. 파일 구조
|
||||
|
||||
```
|
||||
src/
|
||||
├── app/[locale]/(protected)/accounting/
|
||||
│ └── bank-transactions/
|
||||
│ └── page.tsx # 페이지 라우트
|
||||
└── components/accounting/
|
||||
└── BankTransactionInquiry/
|
||||
├── index.tsx # 메인 컴포넌트
|
||||
└── types.ts # 타입 정의
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 참고 사항
|
||||
|
||||
### 5.1 기존 컴포넌트 재사용
|
||||
- `IntegratedListTemplateV2` - 리스트 템플릿
|
||||
- `DateRangeSelector` - 날짜 범위 선택
|
||||
- `StatCard` - 통계 카드
|
||||
- `ListMobileCard` - 모바일 카드
|
||||
|
||||
### 5.2 스크린샷 Description 정보 참고
|
||||
- 필터 탭: 전체(선택), 입금/수입, 출금, 입금, 어제, 오늘
|
||||
- 수정 버튼 클릭 시 입금/출금 상세 화면으로 이동
|
||||
- 종류(정렬): 취입선, 등록순, 입력순
|
||||
|
||||
### 5.3 레이아웃 일치 확인 사항
|
||||
- DepositManagement와 동일한 레이아웃 구조 사용
|
||||
- 카드 4개 가로 배치
|
||||
- 테이블 합계 행 스타일 일치
|
||||
|
||||
---
|
||||
|
||||
## 6. 완료 후 작업
|
||||
|
||||
- [ ] `claudedocs/[REF] all-pages-test-urls.md` 업데이트
|
||||
```markdown
|
||||
| 입출금 계좌조회 | `/ko/accounting/bank-transactions` | 🆕 NEW |
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 예상 소요 시간
|
||||
|
||||
| Phase | 작업 | 예상 |
|
||||
|-------|------|------|
|
||||
| 1-2 | 기본 구조 + 타입 | - |
|
||||
| 3-4 | 카드 + 필터 | - |
|
||||
| 5 | 테이블 | - |
|
||||
| 6-7 | 상세 이동 + Mock | - |
|
||||
| 8 | 마무리 | - |
|
||||
|
||||
---
|
||||
|
||||
**확인 후 작업 시작하겠습니다!**
|
||||
@@ -0,0 +1,313 @@
|
||||
ㅓ# 게시판 관리 기능 구현 계획서
|
||||
|
||||
> 작성일: 2025-12-19
|
||||
> 상태: 🔴 **계획 검토 대기**
|
||||
|
||||
---
|
||||
|
||||
## 1. 개요
|
||||
|
||||
### 1.1 기능 요약
|
||||
게시판 리스트/등록/상세/댓글 기능 구현
|
||||
|
||||
### 1.2 참고 페이지
|
||||
- **탭 네비게이션**: `src/components/items/ItemListClient.tsx` (품목관리)
|
||||
- **테이블 필터 위치**: `src/components/accounting/SalesManagement/index.tsx` (매출관리)
|
||||
- **공통 레이아웃**: `IntegratedListTemplateV2` 템플릿
|
||||
|
||||
### 1.3 디자인 스펙 (스크린샷 기준)
|
||||
|
||||
#### 리스트 페이지
|
||||
| 항목 | 내용 |
|
||||
|------|------|
|
||||
| 페이지 타이틀 | 게시판 |
|
||||
| 페이지 설명 | 게시판의 게시글을 등록하고 관리합니다. |
|
||||
| 날짜 범위 | 2025-09-01 ~ 2025-09-03 |
|
||||
| 탭 | 전체보드, 전자결재, 인쇄, 미역, 우울 + **게시글 등록** 버튼 |
|
||||
| 게시판 필터 | 공지사항 (게시판명, 게시판명2, 나의 게시글) |
|
||||
| 검색 | 검색 입력창 |
|
||||
| 정렬 | 최신순 ▼ |
|
||||
| 테이블 컬럼 | No., 제목, 작성자, 등록일, 조회수 |
|
||||
| 행 클릭 | 게시글 상세 화면으로 이동 |
|
||||
|
||||
#### 등록/수정 페이지
|
||||
| 항목 | 내용 |
|
||||
|------|------|
|
||||
| 페이지 타이틀 | 게시글 상세 |
|
||||
| 페이지 설명 | 게시글을 등록하고 관리합니다. |
|
||||
| **게시글 정보** (필수 섹션) | |
|
||||
| - 게시판 | Select: "게시판을 선택해주세요" |
|
||||
| - 상단 노출 | Radio: 사용안함 / **사용함** |
|
||||
| - 제목 | Input: "제목을 입력해주세요" |
|
||||
| - 내용 | **WYSIWYG 에디터** (B, I, U, S, 정렬, 목록, 링크, 이미지 등) |
|
||||
| - 첨부파일 | 파일 찾기 버튼 |
|
||||
| - 작성자 | 읽기 전용 (예: 홍길동) |
|
||||
| - 댓글 | Radio: **사용안함** / 사용함 |
|
||||
| - 등록일시 | 읽기 전용 (예: 2025-09-09 12:20) |
|
||||
| 상단 노출 제한 | 최대 5개까지 설정 가능, 초과 시 Alert 표시 |
|
||||
|
||||
#### 상세 페이지
|
||||
| 항목 | 내용 |
|
||||
|------|------|
|
||||
| 페이지 타이틀 | 게시글 상세 |
|
||||
| 페이지 설명 | 게시글을 조회합니다. |
|
||||
| 버튼 (본인 글만) | **삭제**, **수정** |
|
||||
| 게시판명 라벨 | 게시판명 |
|
||||
| 제목 | 제목 |
|
||||
| 메타 정보 | 작성자 \| 날짜 \| 조회수 (예: 홍길동 \| 2025-09-03 12:23 \| 조회수 123) |
|
||||
| 내용 | HTML 콘텐츠 (이미지 포함) |
|
||||
| 첨부파일 | 다운로드 링크 (예: abc.pdf) |
|
||||
| 댓글 등록 | Textarea + 등록 버튼 (댓글 사용함 설정 시만 표시) |
|
||||
|
||||
#### 댓글 섹션
|
||||
| 항목 | 내용 |
|
||||
|------|------|
|
||||
| 댓글 수 | 댓글 N |
|
||||
| 댓글 정보 | 프로필 이미지, 부서명 이름 직책, 등록일시, 댓글 내용 |
|
||||
| 수정 버튼 (본인만) | 클릭 시 인풋박스에 기존 댓글 내용 입력 상태로 변경 |
|
||||
| 삭제 버튼 (본인만) | 클릭 시 **"정말 삭제하시겠습니까?"** 확인 Alert 표시 |
|
||||
|
||||
---
|
||||
|
||||
## 2. WYSIWYG 에디터 추천
|
||||
|
||||
### 2.1 옵션 비교
|
||||
|
||||
| 라이브러리 | 장점 | 단점 | 추천도 |
|
||||
|------------|------|------|--------|
|
||||
| **TipTap** | 최신, Headless (커스텀 자유), React 네이티브, shadcn/ui 호환 | 학습 곡선 | ⭐⭐⭐⭐⭐ |
|
||||
| **CKEditor 5** | 기능 풍부, 엔터프라이즈급, 이미지 업로드 내장 | 무거움, 스타일 충돌, 라이선스 | ⭐⭐⭐⭐ |
|
||||
| **Quill** | 간단, 가벼움 | 구식 스타일, 유지보수 부족 | ⭐⭐⭐ |
|
||||
| **Editor.js** | 블록 기반, 노션 스타일 | JSON 출력 (HTML 아님), 변환 필요 | ⭐⭐⭐ |
|
||||
| **React-Quill** | Quill + React 래퍼 | Next.js SSR 이슈 | ⭐⭐ |
|
||||
|
||||
### 2.2 최종 추천: **TipTap**
|
||||
|
||||
**이유**:
|
||||
1. **Headless 아키텍처**: shadcn/ui와 Tailwind CSS 완벽 호환
|
||||
2. **모던 React**: useState/useEffect 패턴, TypeScript 완벽 지원
|
||||
3. **확장성**: 필요한 기능만 설치 (경량화)
|
||||
4. **커뮤니티**: 활발한 개발, 문서화 우수
|
||||
5. **이미지 업로드**: 커스텀 핸들러로 S3/백엔드 연동 용이
|
||||
|
||||
### 2.3 필요 패키지 (TipTap)
|
||||
|
||||
```bash
|
||||
npm install @tiptap/react @tiptap/pm @tiptap/starter-kit \
|
||||
@tiptap/extension-image @tiptap/extension-link \
|
||||
@tiptap/extension-underline @tiptap/extension-text-align \
|
||||
@tiptap/extension-placeholder
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 파일 구조
|
||||
|
||||
```
|
||||
src/
|
||||
├── app/[locale]/(protected)/board/
|
||||
│ ├── page.tsx # 게시판 리스트 페이지
|
||||
│ ├── create/
|
||||
│ │ └── page.tsx # 게시글 등록 페이지
|
||||
│ └── [id]/
|
||||
│ ├── page.tsx # 게시글 상세 페이지
|
||||
│ └── edit/
|
||||
│ └── page.tsx # 게시글 수정 페이지
|
||||
│
|
||||
├── components/board/
|
||||
│ ├── BoardList/
|
||||
│ │ ├── index.tsx # 리스트 메인 컴포넌트
|
||||
│ │ └── types.ts # 타입 정의
|
||||
│ ├── BoardForm/
|
||||
│ │ ├── index.tsx # 등록/수정 폼
|
||||
│ │ └── types.ts
|
||||
│ ├── BoardDetail/
|
||||
│ │ ├── index.tsx # 상세 보기
|
||||
│ │ └── types.ts
|
||||
│ ├── CommentSection/
|
||||
│ │ ├── index.tsx # 댓글 섹션
|
||||
│ │ ├── CommentItem.tsx # 개별 댓글 컴포넌트
|
||||
│ │ └── types.ts
|
||||
│ └── RichTextEditor/
|
||||
│ ├── index.tsx # TipTap 에디터 래퍼
|
||||
│ ├── MenuBar.tsx # 에디터 툴바
|
||||
│ └── extensions.ts # TipTap 확장 설정
|
||||
│
|
||||
└── hooks/
|
||||
└── useBoardList.ts # 게시판 목록 API 훅
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 구현 체크리스트
|
||||
|
||||
### Phase 1: 기반 작업 (에디터 + 타입)
|
||||
|
||||
- [ ] **1.1** TipTap 패키지 설치
|
||||
- [ ] **1.2** `RichTextEditor` 컴포넌트 구현
|
||||
- [ ] 1.2.1 기본 에디터 (Bold, Italic, Underline, Strike)
|
||||
- [ ] 1.2.2 텍스트 정렬 (좌/중/우)
|
||||
- [ ] 1.2.3 목록 (Bullet, Ordered)
|
||||
- [ ] 1.2.4 링크 삽입
|
||||
- [ ] 1.2.5 이미지 업로드 (파일 선택 → 백엔드 업로드 → URL 삽입)
|
||||
- [ ] **1.3** `types.ts` 정의
|
||||
- [ ] 1.3.1 Board (게시판 타입)
|
||||
- [ ] 1.3.2 Post (게시글 타입)
|
||||
- [ ] 1.3.3 Comment (댓글 타입)
|
||||
|
||||
### Phase 2: 리스트 페이지
|
||||
|
||||
- [ ] **2.1** `BoardList/index.tsx` 구현
|
||||
- [ ] 2.1.1 IntegratedListTemplateV2 적용
|
||||
- [ ] 2.1.2 DateRangeSelector (날짜 범위)
|
||||
- [ ] 2.1.3 탭 네비게이션 (전체보드, 전자결재, 인쇄, 미역, 우울)
|
||||
- [ ] 2.1.4 게시판 필터 (공지사항 드롭다운)
|
||||
- [ ] 2.1.5 검색 입력창
|
||||
- [ ] 2.1.6 정렬 드롭다운 (최신순)
|
||||
- [ ] 2.1.7 총 N건 표시
|
||||
- [ ] **2.2** 테이블 구현
|
||||
- [ ] 2.2.1 컬럼: No., 제목, 작성자, 등록일, 조회수
|
||||
- [ ] 2.2.2 체크박스 선택
|
||||
- [ ] 2.2.3 행 클릭 → 상세 이동
|
||||
- [ ] **2.3** 모바일 카드 뷰 구현
|
||||
- [ ] **2.4** 페이지네이션 구현
|
||||
- [ ] **2.5** `page.tsx` 라우트 생성
|
||||
|
||||
### Phase 3: 등록/수정 페이지
|
||||
|
||||
- [ ] **3.1** `BoardForm/index.tsx` 구현
|
||||
- [ ] 3.1.1 게시판 Select (게시판 목록)
|
||||
- [ ] 3.1.2 상단 노출 Radio (사용안함/사용함)
|
||||
- [ ] 3.1.2.1 최대 5개 제한 Alert
|
||||
- [ ] 3.1.3 제목 Input
|
||||
- [ ] 3.1.4 내용 RichTextEditor
|
||||
- [ ] 3.1.5 첨부파일 업로드 (다중 파일)
|
||||
- [ ] 3.1.6 작성자 표시 (읽기 전용)
|
||||
- [ ] 3.1.7 댓글 Radio (사용안함/사용함)
|
||||
- [ ] 3.1.8 등록일시 표시 (읽기 전용)
|
||||
- [ ] 3.1.9 등록 버튼
|
||||
- [ ] **3.2** 수정 모드 구현 (기존 데이터 로드)
|
||||
- [ ] **3.3** 유효성 검사
|
||||
- [ ] 3.3.1 필수 필드: 게시판, 제목, 내용
|
||||
- [ ] **3.4** `create/page.tsx` 라우트 생성
|
||||
- [ ] **3.5** `[id]/edit/page.tsx` 라우트 생성
|
||||
|
||||
### Phase 4: 상세 페이지
|
||||
|
||||
- [ ] **4.1** `BoardDetail/index.tsx` 구현
|
||||
- [ ] 4.1.1 게시판명 라벨
|
||||
- [ ] 4.1.2 제목
|
||||
- [ ] 4.1.3 메타 정보 (작성자 | 날짜 | 조회수)
|
||||
- [ ] 4.1.4 내용 (HTML 렌더링)
|
||||
- [ ] 4.1.5 첨부파일 다운로드 링크
|
||||
- [ ] **4.2** 삭제/수정 버튼 (본인 글만 표시)
|
||||
- [ ] 4.2.1 삭제 버튼 → **AlertDialog** ("정말 삭제하시겠습니까?")
|
||||
- [ ] 4.2.2 수정 버튼 → 수정 페이지 이동
|
||||
- [ ] **4.3** `[id]/page.tsx` 라우트 생성
|
||||
|
||||
### Phase 5: 댓글 기능
|
||||
|
||||
- [ ] **5.1** `CommentSection/index.tsx` 구현
|
||||
- [ ] 5.1.1 댓글 등록 Textarea + 버튼
|
||||
- [ ] 5.1.2 댓글 수 표시 ("댓글 N")
|
||||
- [ ] **5.2** `CommentItem.tsx` 구현
|
||||
- [ ] 5.2.1 프로필 이미지
|
||||
- [ ] 5.2.2 부서명 이름 직책
|
||||
- [ ] 5.2.3 등록일시
|
||||
- [ ] 5.2.4 댓글 내용
|
||||
- [ ] 5.2.5 수정 버튼 (본인만) → 인라인 수정 모드
|
||||
- [ ] 5.2.6 삭제 버튼 (본인만) → **AlertDialog**
|
||||
- [ ] **5.3** 댓글 CRUD API 연동
|
||||
|
||||
### Phase 6: API 연동 + 마무리
|
||||
|
||||
- [ ] **6.1** Mock 데이터 → 실제 API 연동
|
||||
- [ ] **6.2** 에러 핸들링 (toast 알림)
|
||||
- [ ] **6.3** 로딩 상태 UI
|
||||
- [ ] **6.4** 반응형 테스트 (모바일/태블릿/데스크톱)
|
||||
- [ ] **6.5** 접근 권한 테스트 (본인 글/타인 글)
|
||||
|
||||
### Phase 7: 문서화
|
||||
|
||||
- [ ] **7.1** 테스트 URL 문서 업데이트 (`[REF] all-pages-test-urls.md`)
|
||||
- [ ] 7.1.1 게시판 섹션 신규 추가 (기존 구역과 별도)
|
||||
- [ ] 7.1.2 메인 리스트 URL만 등록
|
||||
- [ ] **7.2** 이 문서 완료 처리
|
||||
|
||||
---
|
||||
|
||||
## 5. 주의사항 (버디가 자주 틀리는 것들)
|
||||
|
||||
### 5.1 디스크립션 확인 필수
|
||||
- [ ] 리스트: "게시판의 게시글을 등록하고 관리합니다."
|
||||
- [ ] 등록: "게시글을 등록하고 관리합니다."
|
||||
- [ ] 상세: "게시글을 조회합니다."
|
||||
|
||||
### 5.2 테이블 컬럼 타이틀 정확히
|
||||
- [ ] No. (번호 아님)
|
||||
- [ ] 제목
|
||||
- [ ] 작성자
|
||||
- [ ] 등록일
|
||||
- [ ] 조회수
|
||||
|
||||
### 5.3 카드/라벨 텍스트 정확히
|
||||
- [ ] "게시판" (게시판명 아님, Select label)
|
||||
- [ ] "상단 노출" (상단고정 아님)
|
||||
- [ ] "댓글" (댓글허용 아님)
|
||||
- [ ] "댓글 N" (댓글 수 표시)
|
||||
|
||||
### 5.4 팝업 메시지 정확히
|
||||
- [ ] 삭제 확인: **"정말 삭제하시겠습니까?"**
|
||||
- [ ] 상단 노출 초과: **"상단 노출은 5개까지 설정 가능합니다."**
|
||||
|
||||
### 5.5 본인 글/댓글 체크
|
||||
- [ ] 게시글 삭제/수정 버튼 → 본인 글만
|
||||
- [ ] 댓글 수정/삭제 버튼 → 본인 댓글만
|
||||
|
||||
---
|
||||
|
||||
## 6. 기술 스택
|
||||
|
||||
| 항목 | 기술 |
|
||||
|------|------|
|
||||
| 프레임워크 | Next.js 14 App Router |
|
||||
| UI 컴포넌트 | shadcn/ui |
|
||||
| 스타일링 | Tailwind CSS |
|
||||
| 에디터 | **TipTap** |
|
||||
| 폼 | React Hook Form (권장) |
|
||||
| 상태 관리 | React useState/useCallback |
|
||||
| API | Next.js API Routes (Proxy) |
|
||||
| 팝업 | AlertDialog, Dialog (Radix UI) |
|
||||
|
||||
---
|
||||
|
||||
## 7. 작업 완료 후 필수 조치
|
||||
|
||||
### 7.1 테스트 URL 문서 업데이트
|
||||
|
||||
`claudedocs/[REF] all-pages-test-urls.md` 파일에 다음 내용 추가:
|
||||
|
||||
```markdown
|
||||
## 게시판 (Board) - 🆕 NEW SECTION
|
||||
|
||||
| 페이지 | URL | 비고 |
|
||||
|--------|-----|------|
|
||||
| 게시판 목록 | `/ko/board` | 🆕 NEW |
|
||||
```
|
||||
|
||||
> ⚠️ **참고**: 상세/수정/등록 페이지는 메인 리스트에서 접근 가능하므로 별도 등록하지 않음
|
||||
|
||||
### 7.2 _index.md 업데이트
|
||||
|
||||
`claudedocs/_index.md`에 board/ 폴더 섹션 추가
|
||||
|
||||
---
|
||||
|
||||
## 8. 승인 대기
|
||||
|
||||
- [ ] 사용자 확인 완료
|
||||
- [ ] 작업 시작
|
||||
|
||||
---
|
||||
|
||||
*작성: Claude Code*
|
||||
@@ -0,0 +1,89 @@
|
||||
# [IMPL-2025-12-19] 1:1 문의 관리 구현
|
||||
|
||||
## 개요
|
||||
- **페이지**: 1:1 문의 (고객센터)
|
||||
- **URL**: `/ko/customer-center/inquiries`
|
||||
- **참조**: 공지사항, 이벤트, 게시판 구조
|
||||
|
||||
## 체크리스트
|
||||
|
||||
### Phase 1: 기본 구조
|
||||
- [ ] types.ts 생성 (Inquiry 타입, 필터 옵션 등)
|
||||
- [ ] Mock 데이터 생성
|
||||
|
||||
### Phase 2: 목록 페이지
|
||||
- [ ] InquiryList.tsx 생성
|
||||
- [ ] IntegratedListTemplateV2 사용
|
||||
- [ ] 날짜 범위 선택 (DateRangeSelector)
|
||||
- [ ] 문의 등록 버튼
|
||||
- [ ] 검색창
|
||||
- [ ] 테이블 필터 3개 (상담분류, 상태, 정렬)
|
||||
- [ ] 테이블 컬럼: No., 상담분류, 제목, 상태, 등록일
|
||||
- [ ] page.tsx (목록)
|
||||
|
||||
### Phase 3: 상세 페이지
|
||||
- [ ] InquiryDetail.tsx 생성
|
||||
- [ ] 문의 영역 (제목, 작성자, 날짜, 내용, 첨부파일)
|
||||
- [ ] 답변 영역 (작성자, 날짜, 내용, 첨부파일)
|
||||
- [ ] 댓글 등록 입력창
|
||||
- [ ] 댓글 목록 (프로필, 이름, 내용, 날짜, 수정/삭제)
|
||||
- [ ] 삭제/수정 버튼
|
||||
- [ ] [id]/page.tsx (상세)
|
||||
|
||||
### Phase 4: 등록/수정 페이지
|
||||
- [ ] InquiryForm.tsx 생성
|
||||
- [ ] 상담분류 선택
|
||||
- [ ] 제목 입력
|
||||
- [ ] 내용 에디터 (게시판 에디터 사용)
|
||||
- [ ] 파일 첨부
|
||||
- [ ] create/page.tsx (등록)
|
||||
- [ ] [id]/edit/page.tsx (수정)
|
||||
|
||||
### Phase 5: 마무리
|
||||
- [ ] index.tsx export
|
||||
- [ ] 테스트 URL 문서 업데이트
|
||||
|
||||
## 스펙 상세
|
||||
|
||||
### 목록 페이지
|
||||
| 필드 | 타입 | 설명 |
|
||||
|------|------|------|
|
||||
| 상담분류 필터 | Select | 전체, 문의하기, 신고하기, 건의사항, 서비스오류 |
|
||||
| 상태 필터 | Select | 전체, 답변대기, 답변완료 |
|
||||
| 정렬 | Select | 최신순, 오래된순 |
|
||||
|
||||
### 테이블 컬럼
|
||||
| 컬럼 | 설명 |
|
||||
|------|------|
|
||||
| No. | 번호 |
|
||||
| 상담분류 | 문의하기, 신고하기, 건의사항, 서비스오류 |
|
||||
| 제목 | 문의 제목 |
|
||||
| 상태 | 답변대기, 답변완료 |
|
||||
| 등록일 | YYYY-MM-DD |
|
||||
|
||||
### 상세 페이지 구조
|
||||
1. **문의 영역**
|
||||
- 제목
|
||||
- 작성자 | 등록일시
|
||||
- 내용 (에디터 콘텐츠)
|
||||
- 첨부파일
|
||||
|
||||
2. **답변 영역**
|
||||
- 작성자 | 답변일시
|
||||
- 내용
|
||||
- 첨부파일
|
||||
|
||||
3. **댓글 영역**
|
||||
- 댓글 등록 입력창 + 등록 버튼
|
||||
- 댓글 목록
|
||||
- 프로필 이미지
|
||||
- 이름
|
||||
- 댓글 내용
|
||||
- 등록일시
|
||||
- 수정/삭제 버튼
|
||||
|
||||
## 테스트 URL
|
||||
- 목록: `/ko/customer-center/inquiries`
|
||||
- 상세: `/ko/customer-center/inquiries/[id]`
|
||||
- 등록: `/ko/customer-center/inquiries/create`
|
||||
- 수정: `/ko/customer-center/inquiries/[id]/edit`
|
||||
@@ -0,0 +1,222 @@
|
||||
# [GUIDE-2025-12-16] options vs 평탄화 데이터 패턴
|
||||
|
||||
## 개요
|
||||
|
||||
품목관리 시스템에서 백엔드 API 응답의 `options` 배열과 평탄화된 필드 데이터를 처리하는 패턴에 대한 가이드.
|
||||
|
||||
**핵심 원칙**: `options` 배열을 직접 파싱하지 말고, 백엔드가 정제해서 주는 평탄화된 필드만 사용한다.
|
||||
|
||||
## 배경: 백엔드 데이터 저장 구조
|
||||
|
||||
### 두 가지 저장 방식
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 백엔드 저장 구조 │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌─────────────────┐ │
|
||||
│ │ 동적 필드 │ ────────▶ options (JSON 컬럼) │
|
||||
│ │ (품목기준관리에서 │ [{label, value}, ...] │
|
||||
│ │ 동적으로 생성) │ │
|
||||
│ └─────────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────────┐ │
|
||||
│ │ 고정 필드 │ ────────▶ item_details 테이블 컬럼 │
|
||||
│ │ (하드코딩된 │ bending_details, │
|
||||
│ │ 시스템 필드) │ specification_file 등 │
|
||||
│ └─────────────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### options 배열 구조
|
||||
|
||||
```json
|
||||
{
|
||||
"options": [
|
||||
{ "label": "custom_field_1", "value": "사용자 입력값1" },
|
||||
{ "label": "custom_field_2", "value": "사용자 입력값2" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 평탄화된 응답 구조
|
||||
|
||||
백엔드가 조회 시 정제해서 내려주는 형태:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 123,
|
||||
"code": "FG-001",
|
||||
"name": "제품명",
|
||||
"unit": "EA",
|
||||
"specification": "100x200",
|
||||
"details": {
|
||||
"bending_details": "1110",
|
||||
"specification_file": "https://..."
|
||||
},
|
||||
"options": [...] // ← 이건 무시해야 함!
|
||||
}
|
||||
```
|
||||
|
||||
## 문제: options 직접 파싱의 위험성
|
||||
|
||||
### 발생 원인
|
||||
|
||||
백엔드 `ItemService.php`의 `getKnownFields()` 함수가 `item_details` 테이블 컬럼을 인식하지 못해서, 고정 필드도 `options`에 중복 저장되는 버그가 있었음.
|
||||
|
||||
### 데이터 흐름 (버그 상황)
|
||||
|
||||
```
|
||||
[1차 저장] bending_details = 111
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ item_details.bending_details = 111 ✅ 정상 │
|
||||
│ options = [{label: "bending_details", value: "111"}] ❌ 중복! │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
|
||||
[수정] bending_details = 1110
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ item_details.bending_details = 1110 ✅ 최신값 │
|
||||
│ options = [{label: "bending_details", value: "111"}] ⚠️ 이전값! │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
|
||||
[조회 시 프론트엔드]
|
||||
1. data.bending_details = 1110 가져옴 ✅
|
||||
2. options 순회하며 덮어쓰기...
|
||||
3. formData.bending_details = "111" ❌ 이전값으로 덮어씀!
|
||||
```
|
||||
|
||||
### 증상
|
||||
|
||||
- 품목 수정 후 다시 조회하면 이전 값이 표시됨
|
||||
- 입력한 최신값이 저장은 되지만 화면에 반영 안됨
|
||||
- 특히 `bending_details`, `specification_file`, `certification_file` 등에서 발생
|
||||
|
||||
## 해결: 평탄화 데이터만 사용
|
||||
|
||||
### 올바른 패턴 (현재 코드)
|
||||
|
||||
**파일**: `src/app/[locale]/(protected)/items/[id]/edit/page.tsx`
|
||||
|
||||
```typescript
|
||||
function mapApiResponseToFormData(data: ItemApiResponse): DynamicFormData {
|
||||
const formData: DynamicFormData = {};
|
||||
|
||||
// 1. 백엔드 응답의 최상위 필드를 그대로 복사
|
||||
Object.entries(data).forEach(([key, value]) => {
|
||||
if (!excludeKeys.includes(key) && value !== null) {
|
||||
formData[key] = value;
|
||||
}
|
||||
});
|
||||
|
||||
// 2. details 객체 펼치기 (item_details 테이블 필드)
|
||||
const details = data.details;
|
||||
if (details && typeof details === 'object') {
|
||||
Object.entries(details).forEach(([key, value]) => {
|
||||
if (value !== null && value !== undefined) {
|
||||
formData[key] = value; // 백엔드가 정제한 최신값
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 3. attributes 객체 펼치기 (동적 필드)
|
||||
const attributes = data.attributes || {};
|
||||
Object.entries(attributes).forEach(([key, value]) => {
|
||||
if (!(key in formData)) { // 기존 값 덮어쓰지 않음
|
||||
formData[key] = value;
|
||||
}
|
||||
});
|
||||
|
||||
// ❌ options 파싱 로직 제거!
|
||||
// options는 백엔드 내부 매핑용이므로 프론트엔드에서 사용하지 않음
|
||||
|
||||
return formData;
|
||||
}
|
||||
```
|
||||
|
||||
### 잘못된 패턴 (이전 코드)
|
||||
|
||||
```typescript
|
||||
// ❌ 이렇게 하면 안됨!
|
||||
if (data.options && Array.isArray(data.options)) {
|
||||
data.options.forEach((opt) => {
|
||||
if (opt.label && opt.value) {
|
||||
formData[opt.label] = opt.value; // stale 데이터로 덮어쓸 수 있음!
|
||||
}
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## 데이터 소스 우선순위
|
||||
|
||||
프론트엔드에서 폼 데이터 매핑 시 사용해야 할 데이터 소스 우선순위:
|
||||
|
||||
| 우선순위 | 데이터 소스 | 설명 |
|
||||
|---------|------------|------|
|
||||
| 1 | `data.details.*` | item_details 테이블의 고정 필드 (최신값 보장) |
|
||||
| 2 | `data.*` (최상위) | items/products 테이블 필드 |
|
||||
| 3 | `data.attributes.*` | 동적 필드 (품목기준관리에서 생성) |
|
||||
| ❌ | `data.options[]` | 사용하지 않음 (내부 매핑용) |
|
||||
|
||||
## 영향받는 필드들
|
||||
|
||||
`options`에 잘못 저장될 수 있는 `item_details` 고정 필드들:
|
||||
|
||||
| 필드명 | 설명 | 저장 위치 |
|
||||
|--------|------|----------|
|
||||
| `bending_details` | 전개도 상세 정보 | item_details |
|
||||
| `bending_diagram` | 전개도 이미지 URL | item_details |
|
||||
| `specification_file` | 시방서 파일 URL | item_details |
|
||||
| `certification_file` | 인정서 파일 URL | item_details |
|
||||
| `files` | 첨부파일 목록 | item_details |
|
||||
|
||||
## DropdownField의 options 정규화
|
||||
|
||||
드롭다운 필드에서 사용하는 `options`는 **필드 정의의 선택지 목록**으로, 위에서 말하는 **품목 데이터의 options**와 다름.
|
||||
|
||||
**파일**: `src/components/items/DynamicItemForm/fields/DropdownField.tsx`
|
||||
|
||||
```typescript
|
||||
// 필드 정의의 options (선택지 목록)를 정규화
|
||||
function normalizeOptions(rawOptions: unknown): Array<{ label: string; value: string }> {
|
||||
// 문자열: "옵션1, 옵션2" → [{label, value}, ...]
|
||||
// 배열: ["옵션1", "옵션2"] → [{label, value}, ...]
|
||||
// 객체 배열: [{label, value}, ...] → 그대로 사용
|
||||
}
|
||||
```
|
||||
|
||||
이것은 필드 메타데이터의 선택지를 처리하는 것으로, 품목 데이터의 `options` 배열과 무관함.
|
||||
|
||||
## 관련 파일
|
||||
|
||||
| 파일 | 역할 |
|
||||
|------|------|
|
||||
| `src/app/[locale]/(protected)/items/[id]/edit/page.tsx` | mapApiResponseToFormData() |
|
||||
| `src/app/[locale]/(protected)/items/create/page.tsx` | 생성 페이지 (options 미사용) |
|
||||
| `src/components/items/DynamicItemForm/fields/DropdownField.tsx` | 필드 정의 options 정규화 |
|
||||
| `claudedocs/item-master/[FIX-2025-12-16] options-details-duplicate-bug.md` | 버그 상세 분석 |
|
||||
|
||||
## 향후 계획
|
||||
|
||||
1. **백엔드 근본 수정**: `getKnownFields()`에 `item_details` 컬럼 추가
|
||||
- 이렇게 되면 고정 필드가 `options`에 중복 저장되지 않음
|
||||
|
||||
2. **고정 필드 동적화**: 전개도 상세 등을 품목기준관리에서 동적 필드로 등록
|
||||
- 이 경우 `options`에 저장되는 것이 정상
|
||||
|
||||
3. **프론트엔드 유지**: 현재 패턴 (options 무시)은 백엔드 수정 후에도 안전함
|
||||
|
||||
## 체크리스트
|
||||
|
||||
새로운 API 응답 매핑 코드 작성 시:
|
||||
|
||||
- [ ] `options` 배열을 직접 파싱하지 않았는가?
|
||||
- [ ] `details` 객체를 펼쳐서 최신값을 가져왔는가?
|
||||
- [ ] `attributes` 객체 처리 시 기존 값을 덮어쓰지 않았는가?
|
||||
- [ ] 고정 필드 (`bending_*`, `*_file`)가 올바른 소스에서 오는가?
|
||||
|
||||
## 참고
|
||||
|
||||
- `[FIX-2025-12-16] options-details-duplicate-bug.md` - 버그 발생 원인 상세 분석
|
||||
- `[GUIDE] radix-ui-select-controlled-mode-bug.md` - Radix UI Select 관련 유사 이슈
|
||||
@@ -0,0 +1,169 @@
|
||||
# 페이지 레이아웃 표준화 계획
|
||||
|
||||
## 📋 개요
|
||||
|
||||
**목표**: 품목관리(`/items`) 페이지를 기준으로 모든 페이지의 헤더/레프트 사이드바 간격을 통일
|
||||
|
||||
**기준 페이지**: `/items/page.tsx`
|
||||
```tsx
|
||||
<div className="p-6">
|
||||
<ItemListClient />
|
||||
</div>
|
||||
```
|
||||
|
||||
## 🔍 현재 상황 분석
|
||||
|
||||
### 레이아웃 구조
|
||||
```
|
||||
AuthenticatedLayout
|
||||
├── Header (top, mx-3 mt-3)
|
||||
└── Content Area (flex, gap-3, px-3 pb-3)
|
||||
├── Sidebar (sticky, w-64)
|
||||
└── Main (flex-1, overflow-auto)
|
||||
└── 각 페이지 컴포넌트
|
||||
```
|
||||
|
||||
### 패딩 레이어
|
||||
1. **AuthenticatedLayout**: `<main className="flex-1 overflow-auto">` - 패딩 없음
|
||||
2. **page.tsx wrapper**: `<div className="p-6">` - 품목관리 패턴
|
||||
3. **PageLayout**: `p-3 md:p-6 pb-0` - 내부 패딩
|
||||
|
||||
### 문제점
|
||||
| 구분 | 품목관리 (기준) | 다른 페이지들 |
|
||||
|------|-----------------|--------------|
|
||||
| page.tsx | `<div className="p-6">` | `<Component />` 또는 `<div>` |
|
||||
| 결과 | 헤더/레프트와 적절한 간격 | 헤더/레프트에 붙어있음 |
|
||||
|
||||
## ✅ 수정 대상 페이지 체크리스트
|
||||
|
||||
### 1️⃣ 리스트 페이지 (IntegratedListTemplateV2 사용)
|
||||
- [ ] `/board/page.tsx` - 게시판 목록
|
||||
- [ ] `/hr/employee-management/page.tsx` - 사원관리 목록
|
||||
- [ ] `/hr/attendance-management/page.tsx` - 근태관리 목록
|
||||
- [ ] `/hr/vacation-management/page.tsx` - 휴가관리 목록
|
||||
- [ ] `/accounting/purchase/page.tsx` - 매입관리 목록
|
||||
- [ ] `/accounting/sales/page.tsx` - 매출관리 목록
|
||||
- [ ] `/accounting/vendors/page.tsx` - 거래처관리 목록
|
||||
- [ ] `/accounting/deposits/page.tsx` - 입금관리 목록
|
||||
- [ ] `/accounting/withdrawals/page.tsx` - 출금관리 목록
|
||||
- [ ] `/accounting/bills/page.tsx` - 어음관리 목록
|
||||
- [ ] `/accounting/bank-transactions/page.tsx` - 통장거래내역
|
||||
- [ ] `/accounting/vendor-ledger/page.tsx` - 거래처원장
|
||||
- [ ] `/accounting/daily-report/page.tsx` - 일일마감
|
||||
- [ ] `/accounting/expected-expenses/page.tsx` - 예상지출
|
||||
- [ ] `/accounting/bad-debt-collection/page.tsx` - 악성채권추심
|
||||
- [ ] `/accounting/receivables-status/page.tsx` - 매출채권현황
|
||||
- [ ] `/approval/draft/page.tsx` - 기안함
|
||||
- [ ] `/approval/inbox/page.tsx` - 결재함
|
||||
- [ ] `/approval/reference/page.tsx` - 참조함
|
||||
|
||||
### 2️⃣ 상세/수정/등록 페이지 (PageLayout 직접 사용)
|
||||
- [ ] `/board/create/page.tsx` - 게시글 등록
|
||||
- [ ] `/board/[id]/page.tsx` - 게시글 상세
|
||||
- [ ] `/board/[id]/edit/page.tsx` - 게시글 수정
|
||||
- [ ] `/hr/employee-management/new/page.tsx` - 사원 등록
|
||||
- [ ] `/hr/employee-management/[id]/page.tsx` - 사원 상세
|
||||
- [ ] `/hr/employee-management/[id]/edit/page.tsx` - 사원 수정
|
||||
- [ ] `/accounting/sales/new/page.tsx` - 매출 등록
|
||||
- [ ] `/accounting/sales/[id]/page.tsx` - 매출 상세
|
||||
- [ ] `/accounting/vendors/new/page.tsx` - 거래처 등록
|
||||
- [ ] `/accounting/vendors/[id]/page.tsx` - 거래처 상세
|
||||
- [ ] `/accounting/purchase/[id]/page.tsx` - 매입 상세
|
||||
- [ ] `/accounting/deposits/[id]/page.tsx` - 입금 상세
|
||||
- [ ] `/accounting/withdrawals/[id]/page.tsx` - 출금 상세
|
||||
- [ ] `/accounting/bills/[id]/page.tsx` - 어음 상세
|
||||
- [ ] `/accounting/bills/new/page.tsx` - 어음 등록
|
||||
- [ ] `/accounting/vendor-ledger/[id]/page.tsx` - 거래처원장 상세
|
||||
- [ ] `/accounting/bad-debt-collection/new/page.tsx` - 악성채권 등록
|
||||
- [ ] `/accounting/bad-debt-collection/[id]/page.tsx` - 악성채권 상세
|
||||
- [ ] `/accounting/bad-debt-collection/[id]/edit/page.tsx` - 악성채권 수정
|
||||
- [ ] `/approval/draft/new/page.tsx` - 기안 작성
|
||||
|
||||
### 3️⃣ 기타 페이지
|
||||
- [ ] `/hr/attendance/page.tsx` - 모바일 근태
|
||||
- [ ] `/hr/salary-management/page.tsx` - 급여관리
|
||||
- [ ] `/settings/ranks/page.tsx` - 직급관리
|
||||
- [ ] `/settings/titles/page.tsx` - 직책관리
|
||||
- [ ] `/settings/permissions/page.tsx` - 권한관리
|
||||
- [ ] `/settings/work-schedule/page.tsx` - 근무일정
|
||||
- [ ] `/settings/leave-policy/page.tsx` - 휴가정책
|
||||
- [ ] `/dashboard/page.tsx` - 대시보드
|
||||
|
||||
## 🔧 수정 방법
|
||||
|
||||
### 표준 패턴 (품목관리 기준)
|
||||
|
||||
**page.tsx에서 wrapper 추가:**
|
||||
```tsx
|
||||
// Before
|
||||
export default function SomePage() {
|
||||
return <SomeComponent />;
|
||||
}
|
||||
|
||||
// After
|
||||
export default function SomePage() {
|
||||
return (
|
||||
<div className="p-6">
|
||||
<SomeComponent />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## 📌 공통 레이아웃 래퍼 제안
|
||||
|
||||
향후 관리 편의를 위해 공통 래퍼 컴포넌트 생성:
|
||||
|
||||
```tsx
|
||||
// src/components/organisms/ContentWrapper.tsx
|
||||
export function ContentWrapper({ children }: { children: React.ReactNode }) {
|
||||
return <div className="p-6">{children}</div>;
|
||||
}
|
||||
```
|
||||
|
||||
## 🎯 실행 계획
|
||||
|
||||
### Phase 1: 게시판 페이지 (대표 수정)
|
||||
1. `/board/page.tsx` 수정
|
||||
2. 브라우저에서 확인
|
||||
3. 품목관리와 비교 검증
|
||||
|
||||
### Phase 2: 나머지 페이지 일괄 수정
|
||||
- 체크리스트 기반으로 순차 수정
|
||||
- 각 수정 후 체크 표시
|
||||
|
||||
### Phase 3: RULES.md 업데이트
|
||||
- 페이지 레이아웃 표준 규칙 추가
|
||||
|
||||
## 📝 RULES.md 추가 내용 (예정)
|
||||
|
||||
```markdown
|
||||
## Page Layout Standards
|
||||
**Priority**: 🟡 **Triggers**: 새 페이지 생성, 기존 페이지 레이아웃 수정
|
||||
|
||||
### 표준 패턴
|
||||
- **page.tsx wrapper**: 모든 페이지는 `<div className="p-6">` wrapper 필수
|
||||
- **기준**: 품목관리(`/items/page.tsx`) 페이지
|
||||
|
||||
### 예시
|
||||
\`\`\`tsx
|
||||
// ✅ 올바른 패턴
|
||||
export default function SomePage() {
|
||||
return (
|
||||
<div className="p-6">
|
||||
<SomeComponent />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ❌ 잘못된 패턴
|
||||
export default function SomePage() {
|
||||
return <SomeComponent />;
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
### 패딩 구조
|
||||
- AuthenticatedLayout: 메인 영역 패딩 없음
|
||||
- page.tsx: `p-6` wrapper (24px)
|
||||
- 컴포넌트 내부: 추가 패딩 선택적
|
||||
```
|
||||
206
claudedocs/hr/[IMPL-2025-12-16] mobile-attendance.md
Normal file
206
claudedocs/hr/[IMPL-2025-12-16] mobile-attendance.md
Normal file
@@ -0,0 +1,206 @@
|
||||
# 모바일 출퇴근 시스템 구현 체크리스트
|
||||
|
||||
> Last Updated: 2025-12-18
|
||||
|
||||
## 개요
|
||||
- **목적**: 모바일 기기에서 GPS 기반 출퇴근 기록
|
||||
- **대상**: 특정 사용자 (하드코딩)
|
||||
- **조건**: 지정된 현장 좌표 100m 반경 내에서만 출퇴근 가능
|
||||
|
||||
## 기술 스택
|
||||
- **지도 API**: Google Maps JavaScript API (카카오맵에서 변경)
|
||||
- **API 키**: `.env.local`에 `NEXT_PUBLIC_GOOGLE_MAPS_API_KEY` 저장
|
||||
- **현장 좌표**: `37.557358, 126.864414` (본사)
|
||||
- **반경**: 100m
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: 환경 설정 ✅
|
||||
|
||||
- [x] Google Maps API 키 .env.local에 추가
|
||||
- [x] @types/google.maps 패키지 설치
|
||||
|
||||
## Phase 2: 페이지 구조 ✅
|
||||
|
||||
- [x] `/hr/attendance` 라우트 생성 (기존 protected 레이아웃 활용)
|
||||
- [x] 모바일 전용 레이아웃 (AuthenticatedLayout 모바일 모드 활용)
|
||||
- [x] 페이지 컴포넌트 기본 구조
|
||||
|
||||
## Phase 3: 지도 컴포넌트 ✅
|
||||
|
||||
- [x] GoogleMap 컴포넌트 생성 (`src/components/attendance/GoogleMap.tsx`)
|
||||
- [x] 현장 좌표에 100m 파란 원(Circle) 표시
|
||||
- [x] 현재 위치 마커 표시 (빨간색)
|
||||
- [x] GPS watchPosition으로 실시간 위치 추적
|
||||
- [x] 개발 환경 GPS 시뮬레이션 (localhost에서 본사 근처 50m로 자동 설정)
|
||||
|
||||
## Phase 4: 출퇴근 로직 ✅
|
||||
|
||||
- [x] GPS 거리 계산 함수 (Haversine formula)
|
||||
- [x] 100m 반경 체크 → 버튼 활성화/비활성화
|
||||
- [x] 출근 상태 관리 (출근전/출근중/퇴근완료)
|
||||
- [x] 현재 시간 실시간 표시
|
||||
|
||||
## Phase 5: 완료 화면 ✅
|
||||
|
||||
- [x] 출근 완료 화면 구현 (`src/components/attendance/AttendanceComplete.tsx`)
|
||||
- [x] ✓ 체크 아이콘
|
||||
- [x] "출근 완료" 텍스트 (빨간색)
|
||||
- [x] 시간 표시 (HH:MM:SS)
|
||||
- [x] 날짜 표시 (YYYY년 MM월 DD일 요일)
|
||||
- [x] 위치 표시 (본사)
|
||||
- [x] 확인 버튼
|
||||
- [x] 퇴근 완료 화면 구현 (동일 컴포넌트 재사용)
|
||||
|
||||
## Phase 6: 모바일 감지 & 리다이렉트 ⏳
|
||||
|
||||
- [ ] User-Agent 기반 모바일 감지
|
||||
- [ ] 특정 사용자 하드코딩 체크
|
||||
- [ ] 로그인 후 자동 리다이렉트 로직
|
||||
- [ ] 별도 모바일 레이아웃 (헤더/사이드바 제거)
|
||||
- [ ] 웹에서 접근 차단 (모바일 전용)
|
||||
|
||||
## Phase 7: API 연동 ⏳
|
||||
|
||||
- [ ] 출퇴근 기록 API 설계
|
||||
- [ ] 출근 API (`POST /api/attendance/check-in`)
|
||||
- [ ] 퇴근 API (`POST /api/attendance/check-out`)
|
||||
- [ ] 오늘 출퇴근 상태 조회 API (`GET /api/attendance/today`)
|
||||
- [ ] 현장 좌표 API에서 가져오기 (하드코딩 제거)
|
||||
|
||||
## Phase 8: 사용자 정보 연동 ⏳
|
||||
|
||||
- [ ] 로그인 사용자 정보 연동 (TEST_USER 제거)
|
||||
- [ ] 출퇴근 가능 사용자 권한 체크
|
||||
- [ ] 현장별 사용자 배정 로직
|
||||
|
||||
---
|
||||
|
||||
## 생성된 파일
|
||||
|
||||
| 파일 | 설명 |
|
||||
|------|------|
|
||||
| `src/components/attendance/GoogleMap.tsx` | Google Maps 컴포넌트 (원, 마커, GPS 추적) |
|
||||
| `src/components/attendance/AttendanceComplete.tsx` | 출퇴근 완료 화면 |
|
||||
| `src/app/[locale]/(protected)/hr/attendance/page.tsx` | 출퇴근 메인 페이지 |
|
||||
|
||||
---
|
||||
|
||||
## 테스트 URL
|
||||
|
||||
```
|
||||
http://localhost:3000/ko/hr/attendance
|
||||
```
|
||||
|
||||
모바일 테스트: Chrome DevTools → Toggle Device Toolbar (Ctrl+Shift+M)
|
||||
|
||||
---
|
||||
|
||||
## 하드코딩 설정값 (추후 API로 대체 필요)
|
||||
|
||||
```typescript
|
||||
// 현장 좌표 (본사) - page.tsx:12-17
|
||||
const SITE_LOCATION = {
|
||||
name: '본사',
|
||||
lat: 37.557358,
|
||||
lng: 126.864414,
|
||||
radius: 100, // meters
|
||||
};
|
||||
|
||||
// 테스트용 사용자 정보 - page.tsx:19-23
|
||||
const TEST_USER = {
|
||||
name: '홍길동',
|
||||
department: '부서명',
|
||||
position: '직급명',
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 개발 중 해결한 이슈
|
||||
|
||||
### 1. Hydration 에러
|
||||
- **원인**: 서버/클라이언트 HTML 불일치 (Date, localStorage 등)
|
||||
- **해결**: `mounted` 상태 체크 + `suppressHydrationWarning` 속성
|
||||
|
||||
### 2. Google Maps API 중복 로드
|
||||
- **원인**: React 컴포넌트 리렌더링 시 스크립트 중복 추가
|
||||
- **해결**: `window.googleMapsLoading` 플래그 + 기존 스크립트 체크
|
||||
|
||||
### 3. GPS 권한 거부 (localhost)
|
||||
- **원인**: HTTPS가 아닌 환경에서 GPS 권한 제한
|
||||
- **해결**: 개발 환경 감지 후 테스트 좌표로 시뮬레이션
|
||||
```typescript
|
||||
const isDevelopment =
|
||||
hostname === 'localhost' ||
|
||||
hostname === '127.0.0.1' ||
|
||||
hostname.startsWith('192.168.') ||
|
||||
process.env.NODE_ENV === 'development';
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## UI 스펙 (스크린샷 기반)
|
||||
|
||||
### 출퇴근 메인 화면
|
||||
```
|
||||
┌─────────────────────────────┐
|
||||
│ < 🏠 ⚙️⚙️⚙️ ☰ │ ← 헤더 (AuthenticatedLayout)
|
||||
├─────────────────────────────┤
|
||||
│ 출퇴근하기 │ ← 타이틀
|
||||
├─────────────────────────────┤
|
||||
│ │
|
||||
│ ┌───────────────────┐ │
|
||||
│ │ │ │
|
||||
│ │ 🔵 (100m 원) │ │ ← Google Maps
|
||||
│ │ 📍 │ │
|
||||
│ │ │ │
|
||||
│ └───────────────────┘ │
|
||||
│ │
|
||||
├─────────────────────────────┤
|
||||
│ 👤 홍길동 │
|
||||
│ 부서명 직급명 │
|
||||
│ │
|
||||
│ 08:43:15 (빨간색) │ ← 실시간 시간
|
||||
├─────────────────────────────┤
|
||||
│ [ 출근하기 ] [ 퇴근하기 ]│ ← 버튼
|
||||
└─────────────────────────────┘
|
||||
```
|
||||
|
||||
### 완료 화면
|
||||
```
|
||||
┌─────────────────────────────┐
|
||||
│ < 🏠 ⚙️⚙️⚙️ ☰ │
|
||||
├─────────────────────────────┤
|
||||
│ 출근하기 │
|
||||
├─────────────────────────────┤
|
||||
│ │
|
||||
│ ✓ │ ← 체크 아이콘 (원형)
|
||||
│ │
|
||||
│ 출근 완료 │ ← 빨간색
|
||||
│ 08:43:15 │
|
||||
│ │
|
||||
│ 2025년 12월 15일 (월) │
|
||||
│ │
|
||||
│ 📍 본사 │
|
||||
│ │
|
||||
├─────────────────────────────┤
|
||||
│ [ 확인 ] │
|
||||
└─────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 다음 작업 TODO
|
||||
|
||||
1. **Phase 6 진행**: 모바일 전용 레이아웃 + User-Agent 감지
|
||||
2. **API 설계**: 백엔드 팀과 출퇴근 API 협의
|
||||
3. **테스트**: 실제 모바일 기기에서 GPS 테스트 (HTTPS 환경)
|
||||
|
||||
---
|
||||
|
||||
## 참고 사항
|
||||
|
||||
- MVP 버전: API 연동 없이 하드코딩으로 동작 확인 (현재 상태)
|
||||
- 추후 개선: 출퇴근 기록 API, 현장 좌표 DB 저장, 사용자 권한 체크
|
||||
- Phase 6 (모바일 감지 & 리다이렉트)는 MVP 테스트 후 진행
|
||||
86
claudedocs/hr/[IMPL-2025-12-19] card-management.md
Normal file
86
claudedocs/hr/[IMPL-2025-12-19] card-management.md
Normal file
@@ -0,0 +1,86 @@
|
||||
# [IMPL-2025-12-19] 카드관리 기능 구현
|
||||
|
||||
## 개요
|
||||
- 위치: 기준정보 > 카드관리
|
||||
- 경로: `/hr/card-management`
|
||||
|
||||
## 구현 체크리스트
|
||||
|
||||
### 1. Types 정의
|
||||
- [x] `types.ts` - Card 타입, 상태, 카드사 옵션 정의
|
||||
|
||||
### 2. 컴포넌트 구현
|
||||
- [x] `src/components/hr/CardManagement/index.tsx` - 리스트 컴포넌트 (IntegratedListTemplateV2 사용)
|
||||
- [x] `src/components/hr/CardManagement/CardDetail.tsx` - 상세 컴포넌트
|
||||
- [x] `src/components/hr/CardManagement/CardForm.tsx` - 등록/수정 폼 컴포넌트
|
||||
- [x] `src/components/hr/CardManagement/types.ts` - 타입 정의
|
||||
|
||||
### 3. 페이지 라우팅
|
||||
- [x] `src/app/[locale]/(protected)/hr/card-management/page.tsx` - 리스트 페이지
|
||||
- [x] `src/app/[locale]/(protected)/hr/card-management/[id]/page.tsx` - 상세 페이지
|
||||
- [x] `src/app/[locale]/(protected)/hr/card-management/[id]/edit/page.tsx` - 수정 페이지
|
||||
- [x] `src/app/[locale]/(protected)/hr/card-management/new/page.tsx` - 등록 페이지
|
||||
|
||||
### 4. 문서 업데이트
|
||||
- [x] `claudedocs/[REF] all-pages-test-urls.md` 업데이트
|
||||
|
||||
## 스크린샷 기반 필드 정의
|
||||
|
||||
### 리스트 테이블 컬럼
|
||||
| 컬럼 | 설명 |
|
||||
|------|------|
|
||||
| No. | 번호 |
|
||||
| 카드사 | 카드사 이름 |
|
||||
| 카드번호 | 1234-****-****-1234 형식 |
|
||||
| 카드명 | 카드 명칭 |
|
||||
| 상태 | 사용/정지 |
|
||||
| 부서 | 사용자 부서 |
|
||||
| 사용자 | 사용자 이름 |
|
||||
| 직책 | 사용자 직책 |
|
||||
| 작업 | 선택 시 수정/삭제 버튼 |
|
||||
|
||||
### 상세 페이지 필드
|
||||
|
||||
#### 기본 정보
|
||||
| 필드 | 타입 | 설명 |
|
||||
|------|------|------|
|
||||
| 카드사 | Select | 카드사 선택 |
|
||||
| 카드번호 | Input | 1234-1234-1234-1234 |
|
||||
| 유효기간 | Input | MMYY 형식 |
|
||||
| 카드 비밀번호 앞 2자리 | Input | ** |
|
||||
| 카드명 | Input | 카드 명칭 |
|
||||
| 상태 | Select | 사용/정지 |
|
||||
|
||||
#### 사용자 정보
|
||||
| 필드 | 타입 | 설명 |
|
||||
|------|------|------|
|
||||
| 부서/이름/직책 | Select | 사용자 선택 셀렉트박스 |
|
||||
|
||||
## 진행 상황
|
||||
- 시작일: 2025-12-19
|
||||
- 완료일: 2025-12-19
|
||||
- 현재 상태: 완료
|
||||
|
||||
## 생성된 파일 목록
|
||||
```
|
||||
src/components/hr/CardManagement/
|
||||
├── types.ts # 카드 타입 정의
|
||||
├── index.tsx # 리스트 컴포넌트
|
||||
├── CardDetail.tsx # 상세 컴포넌트
|
||||
└── CardForm.tsx # 등록/수정 폼 컴포넌트
|
||||
|
||||
src/app/[locale]/(protected)/hr/card-management/
|
||||
├── page.tsx # 리스트 페이지
|
||||
├── new/
|
||||
│ └── page.tsx # 등록 페이지
|
||||
└── [id]/
|
||||
├── page.tsx # 상세 페이지
|
||||
└── edit/
|
||||
└── page.tsx # 수정 페이지
|
||||
```
|
||||
|
||||
## 테스트 URL
|
||||
- 리스트: http://localhost:3000/ko/hr/card-management
|
||||
- 등록: http://localhost:3000/ko/hr/card-management/new
|
||||
- 상세: http://localhost:3000/ko/hr/card-management/1
|
||||
- 수정: http://localhost:3000/ko/hr/card-management/1/edit
|
||||
@@ -14,168 +14,184 @@
|
||||
|
||||
## Phase 1: 컴포넌트 분리 (~386줄 감소)
|
||||
|
||||
### 1.1 FormHeader 컴포넌트 분리
|
||||
- [ ] `components/FormHeader.tsx` 파일 생성
|
||||
- [ ] FormHeader 함수 이동 (56-107줄, ~51줄)
|
||||
- [ ] Props 타입 정의
|
||||
- [ ] index.tsx에서 import 및 사용
|
||||
- [ ] 빌드 확인
|
||||
### 1.1 FormHeader 컴포넌트 분리 ✅
|
||||
- [x] `components/FormHeader.tsx` 파일 생성
|
||||
- [x] FormHeader 함수 이동 (56-107줄, ~51줄)
|
||||
- [x] Props 타입 정의
|
||||
- [x] index.tsx에서 import 및 사용
|
||||
- [x] 빌드 확인 (Phase 1 완료 후 일괄)
|
||||
|
||||
### 1.2 ValidationAlert 컴포넌트 분리
|
||||
- [ ] `components/ValidationAlert.tsx` 파일 생성
|
||||
- [ ] ValidationAlert 함수 이동 (112-141줄, ~30줄)
|
||||
- [ ] Props 타입 정의
|
||||
- [ ] index.tsx에서 import 및 사용
|
||||
- [ ] 빌드 확인
|
||||
### 1.2 ValidationAlert 컴포넌트 분리 ✅
|
||||
- [x] `components/ValidationAlert.tsx` 파일 생성
|
||||
- [x] ValidationAlert 함수 이동 (112-141줄, ~30줄)
|
||||
- [x] Props 타입 정의
|
||||
- [x] index.tsx에서 import 및 사용
|
||||
- [x] 빌드 확인 (Phase 1 완료 후 일괄)
|
||||
|
||||
### 1.3 DynamicSectionRenderer 삭제
|
||||
- [ ] 현재 사용 여부 최종 확인
|
||||
- [ ] 미사용 확인 시 코드 삭제 (146-227줄, ~82줄)
|
||||
- [ ] 빌드 확인
|
||||
### 1.3 DynamicSectionRenderer 삭제 ✅
|
||||
- [x] 현재 사용 여부 최종 확인 → 미사용 확인됨
|
||||
- [x] 미사용 확인 시 코드 삭제 (~82줄)
|
||||
- [x] types.ts의 DynamicSectionRendererProps도 삭제
|
||||
- [x] 빌드 확인 (Phase 1 완료 후 일괄)
|
||||
|
||||
### 1.4 FileUploadFields 컴포넌트 분리
|
||||
- [ ] `components/FileUploadFields.tsx` 파일 생성
|
||||
- [ ] 시방서/인정서 업로드 JSX 이동 (1771-1963줄, ~193줄)
|
||||
- [ ] Props 타입 정의 (파일 상태, 핸들러 등)
|
||||
- [ ] index.tsx에서 import 및 사용
|
||||
- [ ] 빌드 확인
|
||||
### 1.4 FileUploadFields 컴포넌트 분리 ✅
|
||||
- [x] `components/FileUploadFields.tsx` 파일 생성 (~220줄)
|
||||
- [x] 시방서/인정서 업로드 JSX 이동 (~173줄 감소)
|
||||
- [x] Props 타입 정의 (FileUploadFieldsProps)
|
||||
- [x] index.tsx에서 import 및 사용
|
||||
- [x] lucide-react 불필요 import 정리
|
||||
- [x] 빌드 확인 (Phase 1 완료 후 일괄)
|
||||
|
||||
### 1.5 DuplicateCodeDialog 컴포넌트 분리
|
||||
- [ ] `components/DuplicateCodeDialog.tsx` 파일 생성
|
||||
- [ ] AlertDialog JSX 이동 (2137-2158줄, ~30줄)
|
||||
- [ ] Props 타입 정의
|
||||
- [ ] index.tsx에서 import 및 사용
|
||||
- [ ] 빌드 확인
|
||||
### 1.5 DuplicateCodeDialog 컴포넌트 분리 ✅
|
||||
- [x] `components/DuplicateCodeDialog.tsx` 파일 생성 (~53줄)
|
||||
- [x] AlertDialog JSX 이동
|
||||
- [x] Props 타입 정의 (DuplicateCodeDialogProps)
|
||||
- [x] index.tsx에서 import 및 사용
|
||||
- [x] 빌드 확인 (Phase 1 완료 후 일괄)
|
||||
|
||||
**Phase 1 완료 후 예상:** ~1,775줄
|
||||
**Phase 1 실제 결과:** 1,842줄 (2,161줄 → 1,842줄, 319줄 감소) ✅
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: 품목코드 생성 훅 분리 (~300줄 감소)
|
||||
## Phase 2: 품목코드 생성 훅 분리 (~300줄 감소) ✅
|
||||
|
||||
### 2.1 useItemCodeGeneration 훅 생성
|
||||
- [ ] `hooks/useItemCodeGeneration.ts` 파일 생성
|
||||
- [ ] 타입 정의 (입력/출력)
|
||||
### 2.1 useItemCodeGeneration 훅 생성 ✅
|
||||
- [x] `hooks/useItemCodeGeneration.ts` 파일 생성 (~420줄)
|
||||
- [x] 타입 정의 (입력/출력) - UseItemCodeGenerationParams, ItemCodeGenerationResult
|
||||
- [x] BendingFieldKeys, AssemblyFieldKeys, PurchasedFieldKeys, CategoryKeyWithId 타입 export
|
||||
|
||||
### 2.2 품목코드 관련 useMemo 이동
|
||||
- [ ] `hasAutoItemCode`, `itemNameKey`, `allSpecificationKeys`, `statusFieldKey` useMemo 이동 (622-674줄)
|
||||
- [ ] `activeSpecificationKey` useMemo 이동 (678-708줄)
|
||||
- [ ] `autoGeneratedItemCode` useMemo 이동 (1234-1254줄)
|
||||
- [ ] 빌드 확인
|
||||
### 2.2 품목코드 관련 useMemo 이동 ✅
|
||||
- [x] `hasAutoItemCode`, `itemNameKey`, `allSpecificationKeys`, `statusFieldKey` useMemo 이동
|
||||
- [x] `activeSpecificationKey` useMemo 이동
|
||||
- [x] `autoGeneratedItemCode` useMemo 이동
|
||||
- [x] 빌드 확인 ✅
|
||||
|
||||
### 2.3 절곡부품 품목코드 로직 이동
|
||||
- [ ] `bendingFieldKeys`, `autoBendingItemCode`, `allCategoryKeysWithIds` useMemo 이동 (837-967줄)
|
||||
- [ ] 빌드 확인
|
||||
### 2.3 절곡부품 품목코드 로직 이동 ✅
|
||||
- [x] `bendingFieldKeys`, `autoBendingItemCode`, `allCategoryKeysWithIds` useMemo 이동
|
||||
- [x] `generateBendingItemCodeSimple` 함수 훅 내부로 이동
|
||||
- [x] 빌드 확인 ✅
|
||||
|
||||
### 2.4 조립부품 품목코드 로직 이동
|
||||
- [ ] `hasAssemblyFields`, `assemblyFieldKeys`, `autoAssemblyItemName`, `autoAssemblySpec` useMemo 이동 (1051-1136줄)
|
||||
- [ ] 빌드 확인
|
||||
### 2.4 조립부품 품목코드 로직 이동 ✅
|
||||
- [x] `hasAssemblyFields`, `assemblyFieldKeys`, `autoAssemblyItemName`, `autoAssemblySpec` useMemo 이동
|
||||
- [x] `generateAssemblyItemNameSimple`, `generateAssemblySpecification` 함수 훅 내부로 이동
|
||||
- [x] 빌드 확인 ✅
|
||||
|
||||
### 2.5 구매부품 품목코드 로직 이동
|
||||
- [ ] `purchasedFieldKeys`, `autoPurchasedItemCode` useMemo 이동 (1140-1227줄)
|
||||
- [ ] 빌드 확인
|
||||
### 2.5 구매부품 품목코드 로직 이동 ✅
|
||||
- [x] `purchasedFieldKeys`, `autoPurchasedItemCode` useMemo 이동
|
||||
- [x] 빌드 확인 ✅
|
||||
|
||||
### 2.6 index.tsx 연결
|
||||
- [ ] useItemCodeGeneration 훅 import
|
||||
- [ ] 기존 useMemo 코드 제거
|
||||
- [ ] 훅 반환값으로 대체
|
||||
- [ ] 빌드 확인
|
||||
- [ ] 기능 테스트 (품목코드 자동생성 동작 확인)
|
||||
### 2.6 index.tsx 연결 ✅
|
||||
- [x] useItemCodeGeneration 훅 import
|
||||
- [x] 기존 useMemo 코드 제거 (4개 블록, ~305줄)
|
||||
- [x] 훅 반환값으로 대체
|
||||
- [x] 빌드 확인 ✅
|
||||
- [ ] 기능 테스트 (품목코드 자동생성 동작 확인) - 수동 테스트 필요
|
||||
|
||||
**Phase 2 완료 후 예상:** ~1,475줄
|
||||
**Phase 2 실제 결과:** 1,432줄 (1,842줄 → 1,432줄, 410줄 감소) ✅
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: 필드 탐지 훅 분리 (~200줄 감소)
|
||||
## Phase 3: 필드 탐지 훅 분리 (~200줄 감소) ✅
|
||||
|
||||
### 3.1 useFieldDetection 훅 생성
|
||||
- [ ] `hooks/useFieldDetection.ts` 파일 생성
|
||||
- [ ] 타입 정의
|
||||
### 3.1 useFieldDetection 훅 생성 ✅
|
||||
- [x] `hooks/useFieldDetection.ts` 파일 생성 (~174줄)
|
||||
- [x] 타입 정의 (PartTypeDetectionResult, UseFieldDetectionParams, FieldDetectionResult)
|
||||
|
||||
### 3.2 부품 유형 필드 탐지 로직 이동
|
||||
- [ ] `partTypeFieldKey`, `selectedPartType`, `isBendingPart`, `isAssemblyPart`, `isPurchasedPart` useMemo 이동 (711-759줄)
|
||||
- [ ] 빌드 확인
|
||||
### 3.2 부품 유형 필드 탐지 로직 이동 ✅
|
||||
- [x] `partTypeFieldKey`, `selectedPartType`, `isBendingPart`, `isAssemblyPart`, `isPurchasedPart` useMemo 이동
|
||||
- [x] 빌드 확인 ✅
|
||||
|
||||
### 3.3 BOM 체크박스 필드 탐지 로직 이동
|
||||
- [ ] `bomRequiredFieldKey` useMemo 이동 (998-1047줄)
|
||||
- [ ] 빌드 확인
|
||||
### 3.3 BOM 체크박스 필드 탐지 로직 이동 ✅
|
||||
- [x] `bomRequiredFieldKey` useMemo 이동
|
||||
- [x] 빌드 확인 ✅
|
||||
|
||||
### 3.4 index.tsx 연결
|
||||
- [ ] useFieldDetection 훅 import
|
||||
- [ ] 기존 useMemo 코드 제거
|
||||
- [ ] 훅 반환값으로 대체
|
||||
- [ ] 빌드 확인
|
||||
- [ ] 기능 테스트 (조건부 필드 표시 확인)
|
||||
### 3.4 index.tsx 연결 ✅
|
||||
- [x] useFieldDetection 훅 import
|
||||
- [x] 기존 useMemo 코드 제거 (2개 블록, ~88줄)
|
||||
- [x] 훅 반환값으로 대체
|
||||
- [x] 빌드 확인 ✅
|
||||
- [ ] 기능 테스트 (조건부 필드 표시 확인) - 수동 테스트 필요
|
||||
|
||||
**Phase 3 완료 후 예상:** ~1,275줄
|
||||
**Phase 3 실제 결과:** 1,344줄 (1,432줄 → 1,344줄, 88줄 감소) ✅
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: 부품 유형 처리 훅 분리 (~150줄 감소)
|
||||
## Phase 4: 부품 유형 처리 훅 분리 (~150줄 감소) ✅
|
||||
|
||||
### 4.1 usePartTypeHandling 훅 생성
|
||||
- [ ] `hooks/usePartTypeHandling.ts` 파일 생성
|
||||
- [ ] 타입 정의
|
||||
### 4.1 usePartTypeHandling 훅 생성 ✅
|
||||
- [x] `hooks/usePartTypeHandling.ts` 파일 생성 (~192줄)
|
||||
- [x] 타입 정의 (UsePartTypeHandlingParams)
|
||||
|
||||
### 4.2 부품 유형 변경 useEffect 이동
|
||||
- [ ] `prevPartTypeRef` 및 부품 유형 변경 감지 useEffect 이동 (762-833줄)
|
||||
- [ ] 빌드 확인
|
||||
### 4.2 부품 유형 변경 useEffect 이동 ✅
|
||||
- [x] `prevPartTypeRef` 및 부품 유형 변경 감지 useEffect 이동
|
||||
- [x] `bendingWidthSumSyncedRef` 및 폭 합계 동기화 useEffect 이동
|
||||
- [x] 빌드 확인 ✅
|
||||
|
||||
### 4.3 품목명 변경 시 종류 초기화 useEffect 이동
|
||||
- [ ] `prevItemNameValueRef` 및 품목명 변경 감지 useEffect 이동 (972-996줄)
|
||||
- [ ] 빌드 확인
|
||||
### 4.3 품목명 변경 시 종류 초기화 useEffect 이동 ✅
|
||||
- [x] `prevItemNameValueRef` 및 품목명 변경 감지 useEffect 이동
|
||||
- [x] 빌드 확인 ✅
|
||||
|
||||
### 4.4 index.tsx 연결
|
||||
- [ ] usePartTypeHandling 훅 import
|
||||
- [ ] 기존 useEffect 코드 제거
|
||||
- [ ] 훅 호출로 대체
|
||||
- [ ] 빌드 확인
|
||||
- [ ] 기능 테스트 (부품 유형 변경 시 필드 초기화 확인)
|
||||
### 4.4 index.tsx 연결 ✅
|
||||
- [x] usePartTypeHandling 훅 import
|
||||
- [x] 기존 useEffect 코드 제거 (~116줄)
|
||||
- [x] 훅 호출로 대체
|
||||
- [x] 빌드 확인 ✅
|
||||
- [ ] 기능 테스트 (부품 유형 변경 시 필드 초기화 확인) - 수동 테스트 필요
|
||||
|
||||
**Phase 4 완료 후 예상:** ~1,125줄
|
||||
**Phase 4 실제 결과:** 1,228줄 (1,344줄 → 1,228줄, 116줄 감소) ✅
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: 파일 처리 훅 분리 (~150줄 감소)
|
||||
## Phase 5: 파일 처리 훅 분리 (~150줄 감소) ✅
|
||||
|
||||
### 5.1 useFileHandling 훅 생성
|
||||
- [ ] `hooks/useFileHandling.ts` 파일 생성
|
||||
- [ ] 타입 정의
|
||||
### 5.1 useFileHandling 훅 생성 ✅
|
||||
- [x] `hooks/useFileHandling.ts` 파일 생성 (~328줄)
|
||||
- [x] 타입 정의 (UseFileHandlingParams, FileHandlingResult)
|
||||
|
||||
### 5.2 파일 상태 및 useEffect 이동
|
||||
- [ ] 파일 관련 state 선언 이동 (274-286줄)
|
||||
- [ ] 파일 정보 로드 useEffect 이동 (294-406줄 중 파일 관련 부분)
|
||||
- [ ] `getDownloadUrl` 함수 이동 (418-422줄)
|
||||
- [ ] `handleDeleteFile` 함수 이동 (425-488줄)
|
||||
- [ ] 빌드 확인
|
||||
### 5.2 파일 상태 및 useEffect 이동 ✅
|
||||
- [x] 파일 관련 state 선언 이동 (existingBendingDiagram, existingSpecificationFile 등)
|
||||
- [x] 파일 정보 로드 useEffect 이동 (initialData에서 files 추출)
|
||||
- [x] bendingDetails 로드 로직 이동
|
||||
- [x] handleFileDownload 함수 이동
|
||||
- [x] handleDeleteFile 함수 이동 (콜백 패턴 적용)
|
||||
- [x] 빌드 확인 ✅
|
||||
|
||||
### 5.3 index.tsx 연결
|
||||
- [ ] useFileHandling 훅 import
|
||||
- [ ] 기존 코드 제거
|
||||
- [ ] 훅 반환값으로 대체
|
||||
- [ ] 빌드 확인
|
||||
- [ ] 기능 테스트 (파일 업로드/삭제/다운로드 확인)
|
||||
### 5.3 index.tsx 연결 ✅
|
||||
- [x] useFileHandling 훅 import
|
||||
- [x] 기존 코드 제거 (~178줄)
|
||||
- [x] 훅 반환값으로 대체
|
||||
- [x] loadedBendingDetails/loadedWidthSum 동기화 useEffect 추가
|
||||
- [x] handleDeleteFile wrapper 함수 추가 (콜백 전달용)
|
||||
- [x] 빌드 확인 ✅
|
||||
- [ ] 기능 테스트 (파일 업로드/삭제/다운로드 확인) - 수동 테스트 필요
|
||||
|
||||
**Phase 5 완료 후 예상:** ~975줄
|
||||
**Phase 5 실제 결과:** 1,050줄 (1,228줄 → 1,050줄, 178줄 감소) ✅
|
||||
|
||||
---
|
||||
|
||||
## Phase 6: 최종 정리 및 검증
|
||||
## Phase 6: 최종 정리 및 검증 ✅
|
||||
|
||||
### 6.1 코드 정리
|
||||
- [ ] 불필요한 import 제거
|
||||
- [ ] 타입 정리 (중복 제거)
|
||||
- [ ] 주석 정리
|
||||
### 6.1 코드 정리 ✅
|
||||
- [x] 불필요한 import 제거 (Button, DynamicSection, DynamicFieldValue, ItemSaveResult)
|
||||
- [x] 미사용 변수 `_` 접두사 처리 (ESLint 경고 해결)
|
||||
- [x] 불필요한 eslint-disable 주석 제거
|
||||
- [x] 브라우저 API (atob, Blob) ESLint 예외 처리
|
||||
|
||||
### 6.2 hooks/index.ts 업데이트
|
||||
- [ ] 새로운 훅들 export 추가
|
||||
### 6.2 hooks/index.ts 업데이트 ✅
|
||||
- [x] useFileHandling export 추가
|
||||
- [x] FileHandlingResult, UseFileHandlingParams 타입 export 추가
|
||||
|
||||
### 6.3 최종 검증
|
||||
- [ ] 빌드 성공 확인
|
||||
- [ ] 타입 에러 없음 확인
|
||||
- [ ] ESLint 경고 확인
|
||||
### 6.3 최종 검증 ✅
|
||||
- [x] 빌드 성공 확인 (`npm run build` 통과)
|
||||
- [x] 타입 에러 없음 확인
|
||||
- [x] ESLint 경고 확인 (0 errors, 2 warnings - 기존 경고)
|
||||
|
||||
### 6.4 기능 테스트 체크리스트
|
||||
### 6.4 기능 테스트 체크리스트 (수동 테스트 필요)
|
||||
- [ ] FG(제품) 등록/수정 테스트
|
||||
- [ ] PT(부품) - 절곡부품 등록/수정 테스트
|
||||
- [ ] PT(부품) - 조립부품 등록/수정 테스트
|
||||
@@ -188,35 +204,62 @@
|
||||
- [ ] 품목코드 자동생성 테스트
|
||||
- [ ] 조건부 필드 표시 테스트
|
||||
|
||||
**Phase 6 완료:** 2025-12-16 ✅
|
||||
|
||||
---
|
||||
|
||||
## 최종 파일 구조
|
||||
## 최종 파일 구조 (실제 결과)
|
||||
|
||||
```
|
||||
src/components/items/DynamicItemForm/
|
||||
├── index.tsx (~900줄, 메인 컴포넌트)
|
||||
├── index.tsx (1,050줄, 메인 컴포넌트) ✅
|
||||
├── components/
|
||||
│ ├── index.ts (배럴 export)
|
||||
│ ├── FormHeader.tsx (~60줄)
|
||||
│ ├── ValidationAlert.tsx (~40줄)
|
||||
│ ├── FileUploadFields.tsx (~200줄)
|
||||
│ └── DuplicateCodeDialog.tsx (~40줄)
|
||||
├── hooks/
|
||||
│ ├── index.ts (기존 + 새 훅 export)
|
||||
│ ├── useFormStructure.ts (기존)
|
||||
│ ├── useDynamicFormState.ts (기존)
|
||||
│ ├── useConditionalDisplay.ts (기존)
|
||||
│ ├── useItemCodeGeneration.ts (~300줄, 신규)
|
||||
│ ├── useFieldDetection.ts (~200줄, 신규)
|
||||
│ ├── usePartTypeHandling.ts (~150줄, 신규)
|
||||
│ └── useFileHandling.ts (~150줄, 신규)
|
||||
│ ├── index.ts (22줄, 배럴 export)
|
||||
│ ├── useFormStructure.ts (95줄, 기존)
|
||||
│ ├── useDynamicFormState.ts (199줄, 기존)
|
||||
│ ├── useConditionalDisplay.ts (182줄, 기존)
|
||||
│ ├── useItemCodeGeneration.ts (523줄, 신규) ✅
|
||||
│ ├── useFieldDetection.ts (174줄, 신규) ✅
|
||||
│ ├── usePartTypeHandling.ts (192줄, 신규) ✅
|
||||
│ └── useFileHandling.ts (328줄, 신규) ✅
|
||||
├── fields/ (기존)
|
||||
├── sections/ (기존)
|
||||
├── types/ (기존)
|
||||
└── utils/ (기존)
|
||||
|
||||
hooks 디렉토리 총: 1,715줄
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 리팩토링 결과 요약
|
||||
|
||||
| Phase | 시작 | 종료 | 감소량 | 상태 |
|
||||
|-------|------|------|--------|------|
|
||||
| Phase 1: 컴포넌트 분리 | 2,161줄 | 1,842줄 | -319줄 | ✅ |
|
||||
| Phase 2: useItemCodeGeneration | 1,842줄 | 1,432줄 | -410줄 | ✅ |
|
||||
| Phase 3: useFieldDetection | 1,432줄 | 1,344줄 | -88줄 | ✅ |
|
||||
| Phase 4: usePartTypeHandling | 1,344줄 | 1,228줄 | -116줄 | ✅ |
|
||||
| Phase 5: useFileHandling | 1,228줄 | 1,050줄 | -178줄 | ✅ |
|
||||
| Phase 6: 최종 정리 | 1,050줄 | 1,050줄 | 0줄 | ✅ |
|
||||
| **총계** | **2,161줄** | **1,050줄** | **-1,111줄 (51% 감소)** | ✅ |
|
||||
|
||||
**최종 결과:**
|
||||
- index.tsx: 2,161줄 → 1,050줄 (51% 감소)
|
||||
- 신규 훅 4개 생성 (1,217줄)
|
||||
- 기존 훅 4개 재사용 (498줄)
|
||||
- 컴포넌트 4개 분리
|
||||
|
||||
**완료일:** 2025-12-16
|
||||
|
||||
---
|
||||
|
||||
## 리스크 및 롤백 계획
|
||||
|
||||
### 리스크 평가
|
||||
|
||||
76
claudedocs/settings/[IMPL-2025-12-19] account-info.md
Normal file
76
claudedocs/settings/[IMPL-2025-12-19] account-info.md
Normal file
@@ -0,0 +1,76 @@
|
||||
# 계정정보 페이지 구현
|
||||
|
||||
> 생성일: 2025-12-19
|
||||
> URL: `/ko/settings/account-info`
|
||||
|
||||
## 📋 체크리스트
|
||||
|
||||
### Phase 1: 기본 구조
|
||||
- [x] page.tsx 생성 (`/settings/account-info/page.tsx`)
|
||||
- [x] AccountInfoClient 컴포넌트 생성
|
||||
|
||||
### Phase 2: 계정 정보 섹션
|
||||
- [x] 프로필 사진 영역 (1250x250px, 10MB 이하 PNG/JPEG/GIF)
|
||||
- [x] 아이디 표시 (읽기 전용)
|
||||
- [x] 비밀번호 영역 + "변경" 버튼
|
||||
- [x] 권한 표시
|
||||
- [x] 상태 표시
|
||||
|
||||
### Phase 3: 약관 동의 정보 섹션
|
||||
- [x] [필수] 서비스 이용약관 동의 + 동의일시
|
||||
- [x] [필수] 개인정보 취급방침 + 동의일시
|
||||
- [x] [선택] 마케팅 정보 수신 동의
|
||||
- [x] 이메일 수신 동의 + 동의일시
|
||||
- [x] SMS 수신 동의 + 동의철회일시
|
||||
|
||||
### Phase 4: 액션 버튼
|
||||
- [x] 탈퇴 버튼 (테넌트 마스터 아닌 경우만 활성화)
|
||||
- [x] 확인 Alert: "정말 탈퇴하시겠습니까?"
|
||||
- [ ] 탈퇴 처리 API 연동 (Mock 구현)
|
||||
- [x] 사용중지 버튼 (테넌트 마스터인 경우만 활성화)
|
||||
- [x] 확인 Alert: "정말 사용중지하시겠습니까?"
|
||||
- [ ] 사용중지 처리 API 연동 (Mock 구현)
|
||||
- [x] 수정 버튼
|
||||
- [x] 비밀번호 변경 버튼 → 비밀번호 설정 화면 이동
|
||||
|
||||
### Phase 5: 마무리
|
||||
- [x] URL 목록 문서 업데이트
|
||||
- [ ] 테스트 및 확인
|
||||
|
||||
---
|
||||
|
||||
## 📝 Description (스크린샷 기준)
|
||||
|
||||
### 탈퇴 버튼
|
||||
- 테넌트 마스터가 아닌 경우에만 버튼 활성화
|
||||
- 클릭: "정말 탈퇴하시겠습니까?" 확인 Alert 표시
|
||||
- 확인 버튼 클릭 시 탈퇴 처리 (모든 테넌트에서 탈퇴 처리, SAM 탈퇴 처리)
|
||||
|
||||
### 사용중지 버튼
|
||||
- 테넌트 마스터인 이면서 경우에만 버튼 활성화
|
||||
- 클릭: "정말 사용중지하시겠습니까?" 확인 Alert 표시
|
||||
- 확인 버튼 클릭 시 사용중지 처리 (해당 테넌트의 사용중지 처리)
|
||||
|
||||
### 변경 버튼
|
||||
- 클릭: 비밀번호 설정 화면으로 이동
|
||||
|
||||
---
|
||||
|
||||
## 🎨 UI 참고
|
||||
|
||||
### 계정 정보 섹션
|
||||
| 필드 | 타입 | 비고 |
|
||||
|------|------|------|
|
||||
| 프로필 사진 | 이미지 업로드 | 1250x250px, 10MB 이하, PNG/JPEG/GIF |
|
||||
| 아이디 | 텍스트 (읽기전용) | abc@email.com |
|
||||
| 비밀번호 | 버튼 | "변경" + 숨김 아이콘 |
|
||||
| 권한 | 텍스트 (읽기전용) | 권한명 |
|
||||
| 상태 | 텍스트 (읽기전용) | 정상 |
|
||||
|
||||
### 약관 동의 정보 섹션
|
||||
| 항목 | 타입 | 동의일시/철회일시 |
|
||||
|------|------|------------------|
|
||||
| [필수] 서비스 이용약관 동의 | 텍스트 | 동의일시 표시 |
|
||||
| [필수] 개인정보 취급방침 | 텍스트 | 동의일시 표시 |
|
||||
| [선택] 이메일 수신 동의 | 체크박스 | 동의일시 표시 |
|
||||
| [선택] SMS 수신 동의 | 체크박스 | 동의철회일시 표시 |
|
||||
@@ -0,0 +1,125 @@
|
||||
# 계좌관리 구현 체크리스트
|
||||
|
||||
> 작성일: 2025-12-19
|
||||
> 경로: 기준정보 > 계좌관리
|
||||
|
||||
---
|
||||
|
||||
## 스크린샷 분석
|
||||
|
||||
### 리스트 페이지 (계좌관리)
|
||||
- **경로**: `/ko/settings/accounts`
|
||||
- **화면명**: 계좌관리
|
||||
- **제목**: 계좌관리
|
||||
- **부제목**: 계좌 목록을 관리합니다
|
||||
|
||||
**테이블 컬럼**:
|
||||
| No. | 컬럼명 | 설명 |
|
||||
|-----|--------|------|
|
||||
| 1 | No. | 번호 (1부터 시작) |
|
||||
| 2 | 은행 | 신한은행, 국민은행, 우리은행 등 |
|
||||
| 3 | 계좌번호 | 1234-****-****-1234 (마스킹) |
|
||||
| 4 | 계좌명 | 계좌명 |
|
||||
| 5 | 예금주 | 예금주명 |
|
||||
| 6 | 작업 | 수정/삭제 버튼 (체크박스 선택 시) |
|
||||
|
||||
**상단 UI**:
|
||||
- 검색창
|
||||
- 페이지 개수 선택 (12개 선택)
|
||||
- 삭제 버튼 (다중 선택)
|
||||
- 계좌 등록 버튼
|
||||
|
||||
**버튼 동작**:
|
||||
- 계좌 등록: 계좌 상세 화면(등록 모드)으로 이동
|
||||
- 삭제 (상단): "선택하신 N개의 계좌를 정말 삭제하시겠습니까?" 확인 팝업
|
||||
- 수정 (행): 계좌 상세 화면으로 이동
|
||||
- 삭제 (행): "계좌를 정말 삭제하시겠습니까?" 확인 팝업
|
||||
|
||||
---
|
||||
|
||||
### 상세 페이지 (계좌 상세)
|
||||
- **경로**: `/ko/settings/accounts/[id]` (상세), `/ko/settings/accounts/new` (등록)
|
||||
- **화면명**: 계좌 상세
|
||||
- **제목**: 계좌 상세
|
||||
- **부제목**: 계좌 정보를 관리합니다
|
||||
|
||||
**상단 버튼**: 삭제, 수정
|
||||
|
||||
**기본 정보 섹션**:
|
||||
| 필드 | 타입 | 설명 |
|
||||
|------|------|------|
|
||||
| 은행 | Dropdown | 은행명 선택 |
|
||||
| 계좌번호 | Text (readonly) | 1234-1234-1234-1234 |
|
||||
| 예금주 | Text | 예금주명 |
|
||||
| 계좌 비밀번호 (빠른 조회 서비스) | Password | 마스킹 처리 |
|
||||
| 계좌명 | Text | 계좌명을 입력해주세요 |
|
||||
| 상태 | Dropdown | 사용/정지 |
|
||||
|
||||
**상태 옵션**:
|
||||
- 사용: 계좌 활성화
|
||||
- 정지: 해당 계좌의 자동 조회 중지
|
||||
|
||||
---
|
||||
|
||||
## 구현 체크리스트
|
||||
|
||||
### Phase 1: 파일 구조 생성
|
||||
- [ ] `src/app/[locale]/(protected)/settings/accounts/page.tsx` 생성
|
||||
- [ ] `src/app/[locale]/(protected)/settings/accounts/[id]/page.tsx` 생성
|
||||
- [ ] `src/app/[locale]/(protected)/settings/accounts/new/page.tsx` 생성
|
||||
- [ ] `src/components/settings/AccountManagement/index.tsx` 생성
|
||||
- [ ] `src/components/settings/AccountManagement/types.ts` 생성
|
||||
- [ ] `src/components/settings/AccountManagement/AccountDetail.tsx` 생성
|
||||
- [ ] `src/components/settings/AccountManagement/AccountForm.tsx` 생성
|
||||
|
||||
### Phase 2: 타입 정의
|
||||
- [ ] Account 인터페이스 정의
|
||||
- [ ] AccountFormData 타입 정의
|
||||
- [ ] 은행 목록 상수 정의
|
||||
- [ ] 상태 옵션 상수 정의
|
||||
|
||||
### Phase 3: 리스트 페이지 구현
|
||||
- [ ] IntegratedListTemplateV2 사용
|
||||
- [ ] 테이블 컬럼 정의 (No., 은행, 계좌번호, 계좌명, 예금주, 작업)
|
||||
- [ ] 계좌번호 마스킹 처리
|
||||
- [ ] 검색 기능
|
||||
- [ ] 페이지네이션
|
||||
- [ ] 다중 선택 삭제 기능
|
||||
- [ ] 계좌 등록 버튼 → /settings/accounts/new 이동
|
||||
- [ ] 수정 버튼 → /settings/accounts/[id] 이동
|
||||
- [ ] 삭제 확인 AlertDialog
|
||||
|
||||
### Phase 4: 상세/등록 페이지 구현
|
||||
- [ ] 등록 페이지 (/new) - AccountForm mode="create"
|
||||
- [ ] 상세 페이지 (/[id]) - AccountDetail + AccountForm mode="view/edit"
|
||||
- [ ] 기본 정보 섹션 레이아웃
|
||||
- [ ] 은행 선택 드롭다운
|
||||
- [ ] 계좌번호 필드 (상세에서 readonly)
|
||||
- [ ] 예금주 필드
|
||||
- [ ] 계좌 비밀번호 필드 (마스킹)
|
||||
- [ ] 계좌명 필드
|
||||
- [ ] 상태 드롭다운 (사용/정지)
|
||||
- [ ] 삭제/수정 버튼
|
||||
|
||||
### Phase 5: Mock 데이터 & 테스트
|
||||
- [ ] Mock 계좌 데이터 생성
|
||||
- [ ] 리스트 페이지 테스트
|
||||
- [ ] 상세 페이지 테스트
|
||||
- [ ] 등록 페이지 테스트
|
||||
- [ ] 삭제 기능 테스트
|
||||
|
||||
### Phase 6: 마무리
|
||||
- [ ] URL 테스트 문서 업데이트
|
||||
- [ ] 체크리스트 완료 표시
|
||||
|
||||
---
|
||||
|
||||
## 참고 사항
|
||||
|
||||
### Description (스크린샷 오른쪽 패널)
|
||||
- 계좌 인증 정보, 비밀번호(빠른 조회 서비스)를 바로결제 API에 전달하여 계좌 내역 자동 수신
|
||||
- 연동 성공 시 해당 계좌의 사용 내역이 자동으로 시스템에 반영됨
|
||||
- 해당 타인에는 은행에서 빠른 조회 서비스 사전 등록 필요
|
||||
|
||||
### 삭제 시 주의사항
|
||||
- 삭제된 계좌의 과거 사용 내역은 보존
|
||||
83
claudedocs/settings/[IMPL-2025-12-19] company-info.md
Normal file
83
claudedocs/settings/[IMPL-2025-12-19] company-info.md
Normal file
@@ -0,0 +1,83 @@
|
||||
# [IMPL-2025-12-19] 회사정보 페이지 구현
|
||||
|
||||
## 개요
|
||||
- **위치**: 보고서 및 분석 > 계정정보 다음 (사이드바 루트 레벨 별도 메뉴)
|
||||
- **경로**: `/ko/company-info`
|
||||
|
||||
## 스크린샷 분석
|
||||
|
||||
### 1. 회사 정보 섹션
|
||||
- [x] 회사 로고 업로드 (750x250px, 10MB 이하, PNG/JPEG/GIF)
|
||||
- [x] 회사명 입력
|
||||
- [x] 대표자명 입력
|
||||
- [x] 업태 입력
|
||||
- [x] 업종 입력
|
||||
- [x] 주소 (우편번호 찾기 버튼, 주소명, 상세주소)
|
||||
- [x] 이메일 (아이디) 입력
|
||||
- [x] 세금계산서 이메일 입력
|
||||
- [x] 담당자명 입력
|
||||
- [x] 담당자 연락처 입력
|
||||
- [x] 사업자등록증 파일 업로드
|
||||
- [x] 사업자등록번호 입력
|
||||
|
||||
### 2. 결제 계좌 정보 섹션
|
||||
- [x] 결제 은행 입력
|
||||
- [x] 계좌 입력
|
||||
- [x] 예금주 입력
|
||||
- [x] 결제일 입력
|
||||
|
||||
### 3. 버튼/기능
|
||||
- [x] 회사 추가 버튼 → 회사 추가 팝업 표시
|
||||
- [x] 수정 버튼
|
||||
|
||||
### 4. 회사 추가 팝업
|
||||
- [x] 사업자등록번호 입력 필드 (숫자만 가능, 10자리)
|
||||
- [x] 취소 버튼
|
||||
- [x] 다음 버튼 (바로빌 API 조회)
|
||||
- 사용 불가 경우: "휴폐업 상태인 사업자입니다." Alert
|
||||
- 등록된 번호: "등록된 사업자등록번호 입니다." Alert
|
||||
- 미등록 번호: "매니저에게 회사 추가 신청 알림을 발송했습니다." Alert
|
||||
|
||||
## 구현 체크리스트
|
||||
|
||||
### Phase 1: 기본 구조
|
||||
- [x] 폴더/파일 생성
|
||||
- [x] `src/app/[locale]/(protected)/company-info/page.tsx`
|
||||
- [x] `src/components/settings/CompanyInfoManagement/index.tsx`
|
||||
- [x] `src/components/settings/CompanyInfoManagement/types.ts`
|
||||
- [x] `src/components/settings/CompanyInfoManagement/AddCompanyDialog.tsx`
|
||||
|
||||
### Phase 2: 컴포넌트 구현
|
||||
- [x] types.ts - 타입 정의
|
||||
- [x] index.tsx - 메인 폼 컴포넌트
|
||||
- [x] AddCompanyDialog.tsx - 회사 추가 팝업
|
||||
|
||||
### Phase 3: 페이지 연결
|
||||
- [x] page.tsx 생성
|
||||
- [ ] API 연동 (TODO)
|
||||
|
||||
## 생성된 파일 목록
|
||||
|
||||
```
|
||||
src/
|
||||
├── app/[locale]/(protected)/company-info/
|
||||
│ └── page.tsx
|
||||
└── components/settings/CompanyInfoManagement/
|
||||
├── index.tsx
|
||||
├── types.ts
|
||||
└── AddCompanyDialog.tsx
|
||||
```
|
||||
|
||||
## 테스트 URL
|
||||
- `/ko/company-info`
|
||||
|
||||
## 참조
|
||||
- 기존 스타일: `AccountDetail.tsx`
|
||||
- 레이아웃: `PageLayout`, `Card` 컴포넌트 사용
|
||||
|
||||
## TODO (API 연동)
|
||||
- [ ] 회사 정보 조회 API
|
||||
- [ ] 회사 정보 수정 API
|
||||
- [ ] 회사 추가 (바로빌 사업자등록번호 조회) API
|
||||
- [ ] 다음 주소 API 연동
|
||||
- [ ] 파일 업로드 API (로고, 사업자등록증)
|
||||
71
claudedocs/settings/[IMPL-2025-12-19] popup-management.md
Normal file
71
claudedocs/settings/[IMPL-2025-12-19] popup-management.md
Normal file
@@ -0,0 +1,71 @@
|
||||
# [IMPL-2025-12-19] 팝업관리 페이지 구현
|
||||
|
||||
> 버디 셋팅 > 팝업관리 페이지 구현
|
||||
|
||||
## 스크린샷 분석
|
||||
|
||||
### 리스트 페이지 (팝업관리)
|
||||
- **테이블 컬럼**: 체크박스, No, 대상, 제목, 상태, 작성자, 등록일, 기간, 작업
|
||||
- **헤더**: "팝업 등록" 버튼
|
||||
- **검색**: 검색창
|
||||
- **선택 삭제**: 2개 이상 선택 시 활성화
|
||||
|
||||
### 상세/등록 페이지 (팝업 상세)
|
||||
- **대상**: Select (전사/부서별)
|
||||
- **기간**: DateRangePicker (시작일~종료일)
|
||||
- **제목**: Input
|
||||
- **내용**: RichTextEditor (게시판과 동일)
|
||||
- **상태**: Radio (사용안함/사용함)
|
||||
- **작성자**: 읽기전용
|
||||
- **등록일시**: 읽기전용
|
||||
|
||||
---
|
||||
|
||||
## 체크리스트
|
||||
|
||||
### Phase 1: 컴포넌트 구조 설정
|
||||
- [x] types.ts 생성 (Popup 타입 정의)
|
||||
- [x] PopupList 컴포넌트 생성
|
||||
- [x] PopupForm 컴포넌트 생성
|
||||
- [x] PopupDetail 컴포넌트 생성
|
||||
|
||||
### Phase 2: 페이지 라우트 생성
|
||||
- [x] /settings/popup-management/page.tsx (리스트)
|
||||
- [x] /settings/popup-management/new/page.tsx (등록)
|
||||
- [x] /settings/popup-management/[id]/page.tsx (상세)
|
||||
- [x] /settings/popup-management/[id]/edit/page.tsx (수정)
|
||||
|
||||
### Phase 3: 마무리
|
||||
- [x] 테스트 URL 문서 업데이트 (all-pages-test-urls.md)
|
||||
- [x] 구현 완료
|
||||
|
||||
---
|
||||
|
||||
## 구현 세부사항
|
||||
|
||||
### 경로 구조
|
||||
```
|
||||
/ko/settings/popup-management → 리스트
|
||||
/ko/settings/popup-management/new → 등록
|
||||
/ko/settings/popup-management/[id] → 상세
|
||||
/ko/settings/popup-management/[id]/edit → 수정
|
||||
```
|
||||
|
||||
### 참고 컴포넌트
|
||||
- 리스트: IntegratedListTemplateV2
|
||||
- 에디터: /components/board/RichTextEditor
|
||||
- 폼 패턴: /components/board/BoardForm
|
||||
|
||||
---
|
||||
|
||||
## 작업 로그
|
||||
|
||||
| 시간 | 작업 | 상태 |
|
||||
|------|------|------|
|
||||
| 시작 | 체크리스트 문서 생성 | ✅ |
|
||||
| | types.ts 생성 | ✅ |
|
||||
| | PopupList 컴포넌트 | ✅ |
|
||||
| | PopupForm 컴포넌트 | ✅ |
|
||||
| | PopupDetail 컴포넌트 | ✅ |
|
||||
| | page.tsx 라우트 생성 | ✅ |
|
||||
| | 테스트 URL 업데이트 | ✅ |
|
||||
@@ -0,0 +1,71 @@
|
||||
# 구독관리 페이지 구현
|
||||
|
||||
> 작성일: 2025-12-19
|
||||
> URL: `/ko/settings/subscription`
|
||||
|
||||
## 스크린샷 분석
|
||||
|
||||
### 페이지 구조
|
||||
- **제목**: 구독관리
|
||||
- **부제목**: 구독 정보를 관리합니다
|
||||
- **테넌트 마스터에게만 표시**
|
||||
|
||||
### 상단 버튼
|
||||
1. **자료 내보내기** (01) - 클릭: 자료 다운로드 처리
|
||||
2. **서비스 해지** (02) - 클릭: "모든 데이터가 삭제되며 복구할 수 없습니다. 정말 서비스를 해지하시겠습니까?" 확인 Alert 표시, 확인 버튼 클릭 시 서비스 해지 처리
|
||||
|
||||
### 구독 정보 카드 (3개 가로 배열)
|
||||
| 항목 | 값 |
|
||||
|------|-----|
|
||||
| 최근 결제일시 | 2025년 12월 1일 |
|
||||
| 다음 결제일시 | 2025년 12월 1일 |
|
||||
| 구독금액 | 500,000원 |
|
||||
|
||||
### 구독 정보 영역 (03)
|
||||
| 항목 | 진행률 | 값 |
|
||||
|------|--------|-----|
|
||||
| 플랜 | - | 프리미엄 |
|
||||
| 사용자 수 | Progress Bar | 100명 / 무제한 |
|
||||
| 저장 공간 | Progress Bar | 5.5 TB / 10 TB |
|
||||
| AI API 호출 | Progress Bar | 8,500 / 10,000 |
|
||||
|
||||
---
|
||||
|
||||
## 체크리스트
|
||||
|
||||
### Phase 1: 기본 구조
|
||||
- [x] page.tsx 생성 (`/settings/subscription`)
|
||||
- [x] SubscriptionManagement 컴포넌트 생성
|
||||
- [x] types.ts 정의
|
||||
|
||||
### Phase 2: UI 구현
|
||||
- [x] PageLayout 적용
|
||||
- [x] 헤더 영역 (제목 + 버튼)
|
||||
- [x] 구독 정보 카드 3개 (최근결제, 다음결제, 금액)
|
||||
- [x] 구독 정보 영역 (플랜 + Progress Bar)
|
||||
|
||||
### Phase 3: 기능 구현
|
||||
- [x] 자료 내보내기 버튼 핸들러
|
||||
- [x] 서비스 해지 AlertDialog 구현
|
||||
- [x] Mock 데이터 연결
|
||||
|
||||
### Phase 4: 마무리
|
||||
- [x] URL 문서 업데이트
|
||||
- [ ] 테스트
|
||||
|
||||
---
|
||||
|
||||
## 생성된 파일
|
||||
|
||||
```
|
||||
src/
|
||||
├── app/[locale]/(protected)/settings/subscription/
|
||||
│ └── page.tsx
|
||||
└── components/settings/SubscriptionManagement/
|
||||
├── index.tsx
|
||||
└── types.ts
|
||||
```
|
||||
|
||||
## 테스트 URL
|
||||
|
||||
http://localhost:3000/ko/settings/subscription
|
||||
815
package-lock.json
generated
815
package-lock.json
generated
@@ -24,6 +24,14 @@
|
||||
"@radix-ui/react-switch": "^1.2.6",
|
||||
"@radix-ui/react-tabs": "^1.1.13",
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
"@tiptap/extension-image": "^3.13.0",
|
||||
"@tiptap/extension-link": "^3.13.0",
|
||||
"@tiptap/extension-placeholder": "^3.13.0",
|
||||
"@tiptap/extension-text-align": "^3.13.0",
|
||||
"@tiptap/extension-underline": "^3.13.0",
|
||||
"@tiptap/pm": "^3.13.0",
|
||||
"@tiptap/react": "^3.13.0",
|
||||
"@tiptap/starter-kit": "^3.13.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
@@ -45,6 +53,7 @@
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.57.0",
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@types/google.maps": "^3.58.1",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
@@ -3332,6 +3341,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@remirror/core-constants": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@remirror/core-constants/-/core-constants-3.0.0.tgz",
|
||||
"integrity": "sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@rtsao/scc": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
|
||||
@@ -3644,6 +3659,479 @@
|
||||
"tailwindcss": "4.1.16"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/core": {
|
||||
"version": "3.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.13.0.tgz",
|
||||
"integrity": "sha512-iUelgiTMgPVMpY5ZqASUpk8mC8HuR9FWKaDzK27w9oWip9tuB54Z8mePTxNcQaSPb6ErzEaC8x8egrRt7OsdGQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/pm": "^3.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-blockquote": {
|
||||
"version": "3.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-blockquote/-/extension-blockquote-3.13.0.tgz",
|
||||
"integrity": "sha512-K1z/PAIIwEmiWbzrP//4cC7iG1TZknDlF1yb42G7qkx2S2X4P0NiqX7sKOej3yqrPjKjGwPujLMSuDnCF87QkQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "^3.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-bold": {
|
||||
"version": "3.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-3.13.0.tgz",
|
||||
"integrity": "sha512-VYiDN9EEwR6ShaDLclG8mphkb/wlIzqfk7hxaKboq1G+NSDj8PcaSI9hldKKtTCLeaSNu6UR5nkdu/YHdzYWTw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "^3.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-bubble-menu": {
|
||||
"version": "3.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-3.13.0.tgz",
|
||||
"integrity": "sha512-qZ3j2DBsqP9DjG2UlExQ+tHMRhAnWlCKNreKddKocb/nAFrPdBCtvkqIEu+68zPlbLD4ukpoyjUklRJg+NipFg==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@floating-ui/dom": "^1.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "^3.13.0",
|
||||
"@tiptap/pm": "^3.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-bullet-list": {
|
||||
"version": "3.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-3.13.0.tgz",
|
||||
"integrity": "sha512-fFQmmEUoPzRGiQJ/KKutG35ZX21GE+1UCDo8Q6PoWH7Al9lex47nvyeU1BiDYOhcTKgIaJRtEH5lInsOsRJcSA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/extension-list": "^3.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-code": {
|
||||
"version": "3.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-3.13.0.tgz",
|
||||
"integrity": "sha512-sF5raBni6iSVpXWvwJCAcOXw5/kZ+djDHx1YSGWhopm4+fsj0xW7GvVO+VTwiFjZGKSw+K5NeAxzcQTJZd3Vhw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "^3.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-code-block": {
|
||||
"version": "3.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-3.13.0.tgz",
|
||||
"integrity": "sha512-kIwfQ4iqootsWg9e74iYJK54/YMIj6ahUxEltjZRML5z/h4gTDcQt2eTpnEC8yjDjHeUVOR94zH9auCySyk9CQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "^3.13.0",
|
||||
"@tiptap/pm": "^3.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-document": {
|
||||
"version": "3.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-3.13.0.tgz",
|
||||
"integrity": "sha512-RjU7hTJwjKXIdY57o/Pc+Yr8swLkrwT7PBQ/m+LCX5oO/V2wYoWCjoBYnK5KSHrWlNy/aLzC33BvLeqZZ9nzlQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "^3.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-dropcursor": {
|
||||
"version": "3.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-3.13.0.tgz",
|
||||
"integrity": "sha512-m7GPT3c/83ni+bbU8c+3dpNa8ug+aQ4phNB1Q52VQG3oTonDJnZS7WCtn3lB/Hi1LqoqMtEHwhepU2eD+JeXqQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/extensions": "^3.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-floating-menu": {
|
||||
"version": "3.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-3.13.0.tgz",
|
||||
"integrity": "sha512-OsezV2cMofZM4c13gvgi93IEYBUzZgnu8BXTYZQiQYekz4bX4uulBmLa1KOA9EN71FzS+SoLkXHU0YzlbLjlxA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@floating-ui/dom": "^1.0.0",
|
||||
"@tiptap/core": "^3.13.0",
|
||||
"@tiptap/pm": "^3.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-gapcursor": {
|
||||
"version": "3.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-3.13.0.tgz",
|
||||
"integrity": "sha512-KVxjQKkd964nin+1IdM2Dvej/Jy4JTMcMgq5seusUhJ9T9P8F9s2D5Iefwgkps3OCzub/aF+eAsZe+1P5KSIgA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/extensions": "^3.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-hard-break": {
|
||||
"version": "3.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-3.13.0.tgz",
|
||||
"integrity": "sha512-nH1OBaO+/pakhu+P1jF208mPgB70IKlrR/9d46RMYoYbqJTNf4KVLx5lHAOHytIhjcNg+MjyTfJWfkK+dyCCyg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "^3.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-heading": {
|
||||
"version": "3.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-3.13.0.tgz",
|
||||
"integrity": "sha512-8VKWX8waYPtUWN97J89em9fOtxNteh6pvUEd0htcOAtoxjt2uZjbW5N4lKyWhNKifZBrVhH2Cc2NUPuftCVgxw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "^3.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-horizontal-rule": {
|
||||
"version": "3.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-3.13.0.tgz",
|
||||
"integrity": "sha512-ZUFyORtjj22ib8ykbxRhWFQOTZjNKqOsMQjaAGof30cuD2DN5J5pMz7Haj2fFRtLpugWYH+f0Mi+WumQXC3hCw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "^3.13.0",
|
||||
"@tiptap/pm": "^3.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-image": {
|
||||
"version": "3.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-image/-/extension-image-3.13.0.tgz",
|
||||
"integrity": "sha512-223uzLUkIa1rkK7aQK3AcIXe6LbCtmnpVb7sY5OEp+LpSaSPyXwyrZ4A0EO1o98qXG68/0B2OqMntFtA9c5Fbw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "^3.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-italic": {
|
||||
"version": "3.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-3.13.0.tgz",
|
||||
"integrity": "sha512-XbVTgmzk1kgUMTirA6AGdLTcKHUvEJoh3R4qMdPtwwygEOe7sBuvKuLtF6AwUtpnOM+Y3tfWUTNEDWv9AcEdww==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "^3.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-link": {
|
||||
"version": "3.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-link/-/extension-link-3.13.0.tgz",
|
||||
"integrity": "sha512-LuFPJ5GoL12GHW4A+USsj60O90pLcwUPdvEUSWewl9USyG6gnLnY/j5ZOXPYH7LiwYW8+lhq7ABwrDF2PKyBbA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"linkifyjs": "^4.3.2"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "^3.13.0",
|
||||
"@tiptap/pm": "^3.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-list": {
|
||||
"version": "3.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-list/-/extension-list-3.13.0.tgz",
|
||||
"integrity": "sha512-MMFH0jQ4LeCPkJJFyZ77kt6eM/vcKujvTbMzW1xSHCIEA6s4lEcx9QdZMPpfmnOvTzeoVKR4nsu2t2qT9ZXzAw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "^3.13.0",
|
||||
"@tiptap/pm": "^3.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-list-item": {
|
||||
"version": "3.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-3.13.0.tgz",
|
||||
"integrity": "sha512-63NbcS/XeQP2jcdDEnEAE3rjJICDj8y1SN1h/MsJmSt1LusnEo8WQ2ub86QELO6XnD3M04V03cY6Knf6I5mTkw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/extension-list": "^3.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-list-keymap": {
|
||||
"version": "3.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-list-keymap/-/extension-list-keymap-3.13.0.tgz",
|
||||
"integrity": "sha512-P+HtIa1iwosb1feFc8B/9MN5EAwzS+/dZ0UH0CTF2E4wnp5Z9OMxKl1IYjfiCwHzZrU5Let+S/maOvJR/EmV0g==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/extension-list": "^3.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-ordered-list": {
|
||||
"version": "3.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-3.13.0.tgz",
|
||||
"integrity": "sha512-QuDyLzuK/3vCvx9GeKhgvHWrGECBzmJyAx6gli2HY+Iil7XicbfltV4nvhIxgxzpx3LDHLKzJN9pBi+2MzX60g==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/extension-list": "^3.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-paragraph": {
|
||||
"version": "3.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-3.13.0.tgz",
|
||||
"integrity": "sha512-9csQde1i0yeZI5oQQ9e1GYNtGL2JcC2d8Fwtw9FsGC8yz2W0h+Fmk+3bc2kobbtO5LGqupSc1fKM8fAg5rSRDg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "^3.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-placeholder": {
|
||||
"version": "3.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-placeholder/-/extension-placeholder-3.13.0.tgz",
|
||||
"integrity": "sha512-Au4ktRBraQktX9gjSzGWyJV6kPof7+kOhzE8ej+rOMjIrHbx3DCHy1CJWftSO9BbqIyonjsFmm4nE+vjzZ3Z5Q==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/extensions": "^3.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-strike": {
|
||||
"version": "3.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-3.13.0.tgz",
|
||||
"integrity": "sha512-VHhWNqTAMOfrC48m2FcPIZB0nhl6XHQviAV16SBc+EFznKNv9tQUsqQrnuQ2y6ZVfqq5UxvZ3hKF/JlN/Ff7xw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "^3.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-text": {
|
||||
"version": "3.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-3.13.0.tgz",
|
||||
"integrity": "sha512-VcZIna93rixw7hRkHGCxDbL3kvJWi80vIT25a2pXg0WP1e7Pi3nBYvZIL4SQtkbBCji9EHrbZx3p8nNPzfazYw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "^3.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-text-align": {
|
||||
"version": "3.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-text-align/-/extension-text-align-3.13.0.tgz",
|
||||
"integrity": "sha512-hebIus9tdXWb+AmhO+LTeUxZLdb0tqwdeaL/0wYxJQR5DeCTlJe6huXacMD/BkmnlEpRhxzQH0FrmXAd0d4Wgg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "^3.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-underline": {
|
||||
"version": "3.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-underline/-/extension-underline-3.13.0.tgz",
|
||||
"integrity": "sha512-VDQi+UYw0tFnfghpthJTFmtJ3yx90kXeDwFvhmT8G+O+si5VmP05xYDBYBmYCix5jqKigJxEASiBL0gYOgMDEg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "^3.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extensions": {
|
||||
"version": "3.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extensions/-/extensions-3.13.0.tgz",
|
||||
"integrity": "sha512-i7O0ptSibEtTy+2PIPsNKEvhTvMaFJg1W4Oxfnbuxvaigs7cJV9Q0lwDUcc7CPsNw2T1+44wcxg431CzTvdYoA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "^3.13.0",
|
||||
"@tiptap/pm": "^3.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/pm": {
|
||||
"version": "3.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.13.0.tgz",
|
||||
"integrity": "sha512-WKR4ucALq+lwx0WJZW17CspeTpXorbIOpvKv5mulZica6QxqfMhn8n1IXCkDws/mCoLRx4Drk5d377tIjFNsvQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"prosemirror-changeset": "^2.3.0",
|
||||
"prosemirror-collab": "^1.3.1",
|
||||
"prosemirror-commands": "^1.6.2",
|
||||
"prosemirror-dropcursor": "^1.8.1",
|
||||
"prosemirror-gapcursor": "^1.3.2",
|
||||
"prosemirror-history": "^1.4.1",
|
||||
"prosemirror-inputrules": "^1.4.0",
|
||||
"prosemirror-keymap": "^1.2.2",
|
||||
"prosemirror-markdown": "^1.13.1",
|
||||
"prosemirror-menu": "^1.2.4",
|
||||
"prosemirror-model": "^1.24.1",
|
||||
"prosemirror-schema-basic": "^1.2.3",
|
||||
"prosemirror-schema-list": "^1.5.0",
|
||||
"prosemirror-state": "^1.4.3",
|
||||
"prosemirror-tables": "^1.6.4",
|
||||
"prosemirror-trailing-node": "^3.0.0",
|
||||
"prosemirror-transform": "^1.10.2",
|
||||
"prosemirror-view": "^1.38.1"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/react": {
|
||||
"version": "3.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/react/-/react-3.13.0.tgz",
|
||||
"integrity": "sha512-VqpqNZ9qtPr3pWK4NsZYxXgLSEiAnzl6oS7tEGmkkvJbcGSC+F7R13Xc9twv/zT5QCLxaHdEbmxHbuAIkrMgJQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/use-sync-external-store": "^0.0.6",
|
||||
"fast-equals": "^5.3.3",
|
||||
"use-sync-external-store": "^1.4.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@tiptap/extension-bubble-menu": "^3.13.0",
|
||||
"@tiptap/extension-floating-menu": "^3.13.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "^3.13.0",
|
||||
"@tiptap/pm": "^3.13.0",
|
||||
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"@types/react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/starter-kit": {
|
||||
"version": "3.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/starter-kit/-/starter-kit-3.13.0.tgz",
|
||||
"integrity": "sha512-Ojn6sRub04CRuyQ+9wqN62JUOMv+rG1vXhc2s6DCBCpu28lkCMMW+vTe7kXJcEdbot82+5swPbERw9vohswFzg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tiptap/core": "^3.13.0",
|
||||
"@tiptap/extension-blockquote": "^3.13.0",
|
||||
"@tiptap/extension-bold": "^3.13.0",
|
||||
"@tiptap/extension-bullet-list": "^3.13.0",
|
||||
"@tiptap/extension-code": "^3.13.0",
|
||||
"@tiptap/extension-code-block": "^3.13.0",
|
||||
"@tiptap/extension-document": "^3.13.0",
|
||||
"@tiptap/extension-dropcursor": "^3.13.0",
|
||||
"@tiptap/extension-gapcursor": "^3.13.0",
|
||||
"@tiptap/extension-hard-break": "^3.13.0",
|
||||
"@tiptap/extension-heading": "^3.13.0",
|
||||
"@tiptap/extension-horizontal-rule": "^3.13.0",
|
||||
"@tiptap/extension-italic": "^3.13.0",
|
||||
"@tiptap/extension-link": "^3.13.0",
|
||||
"@tiptap/extension-list": "^3.13.0",
|
||||
"@tiptap/extension-list-item": "^3.13.0",
|
||||
"@tiptap/extension-list-keymap": "^3.13.0",
|
||||
"@tiptap/extension-ordered-list": "^3.13.0",
|
||||
"@tiptap/extension-paragraph": "^3.13.0",
|
||||
"@tiptap/extension-strike": "^3.13.0",
|
||||
"@tiptap/extension-text": "^3.13.0",
|
||||
"@tiptap/extension-underline": "^3.13.0",
|
||||
"@tiptap/extensions": "^3.13.0",
|
||||
"@tiptap/pm": "^3.13.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
}
|
||||
},
|
||||
"node_modules/@tybys/wasm-util": {
|
||||
"version": "0.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
|
||||
@@ -3725,6 +4213,13 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/google.maps": {
|
||||
"version": "3.58.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/google.maps/-/google.maps-3.58.1.tgz",
|
||||
"integrity": "sha512-X9QTSvGJ0nCfMzYOnaVs/k6/4L+7F5uCS+4iUmkLEls6J9S/Phv+m/i3mDeyc49ZBgwab3EFO1HEoBY7k98EGQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/json-schema": {
|
||||
"version": "7.0.15",
|
||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
||||
@@ -3739,6 +4234,28 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/linkify-it": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz",
|
||||
"integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/markdown-it": {
|
||||
"version": "14.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz",
|
||||
"integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/linkify-it": "^5",
|
||||
"@types/mdurl": "^2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/mdurl": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz",
|
||||
"integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "20.19.24",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.24.tgz",
|
||||
@@ -3753,7 +4270,6 @@
|
||||
"version": "19.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz",
|
||||
"integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"csstype": "^3.0.2"
|
||||
@@ -3763,7 +4279,6 @@
|
||||
"version": "19.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.2.tgz",
|
||||
"integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "^19.2.0"
|
||||
@@ -4405,7 +4920,6 @@
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
||||
"dev": true,
|
||||
"license": "Python-2.0"
|
||||
},
|
||||
"node_modules/aria-hidden": {
|
||||
@@ -4841,6 +5355,12 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/crelt": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
|
||||
"integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
@@ -4860,7 +5380,6 @@
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/d3-array": {
|
||||
@@ -5199,6 +5718,18 @@
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/entities": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
||||
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/es-abstract": {
|
||||
"version": "1.24.0",
|
||||
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz",
|
||||
@@ -5390,7 +5921,6 @@
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
|
||||
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
@@ -5817,6 +6347,15 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-equals": {
|
||||
"version": "5.4.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.4.0.tgz",
|
||||
"integrity": "sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-glob": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz",
|
||||
@@ -7162,6 +7701,21 @@
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/linkify-it": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz",
|
||||
"integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"uc.micro": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/linkifyjs": {
|
||||
"version": "4.3.2",
|
||||
"resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.3.2.tgz",
|
||||
"integrity": "sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/locate-path": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
|
||||
@@ -7217,6 +7771,23 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.5.5"
|
||||
}
|
||||
},
|
||||
"node_modules/markdown-it": {
|
||||
"version": "14.1.0",
|
||||
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz",
|
||||
"integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"argparse": "^2.0.1",
|
||||
"entities": "^4.4.0",
|
||||
"linkify-it": "^5.0.0",
|
||||
"mdurl": "^2.0.0",
|
||||
"punycode.js": "^2.3.1",
|
||||
"uc.micro": "^2.1.0"
|
||||
},
|
||||
"bin": {
|
||||
"markdown-it": "bin/markdown-it.mjs"
|
||||
}
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
@@ -7227,6 +7798,12 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/mdurl": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz",
|
||||
"integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/merge2": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
||||
@@ -7579,6 +8156,12 @@
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/orderedmap": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/orderedmap/-/orderedmap-2.1.1.tgz",
|
||||
"integrity": "sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/own-keys": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz",
|
||||
@@ -7781,6 +8364,201 @@
|
||||
"react-is": "^16.13.1"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-changeset": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-changeset/-/prosemirror-changeset-2.3.1.tgz",
|
||||
"integrity": "sha512-j0kORIBm8ayJNl3zQvD1TTPHJX3g042et6y/KQhZhnPrruO8exkTgG8X+NRpj7kIyMMEx74Xb3DyMIBtO0IKkQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"prosemirror-transform": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-collab": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-collab/-/prosemirror-collab-1.3.1.tgz",
|
||||
"integrity": "sha512-4SnynYR9TTYaQVXd/ieUvsVV4PDMBzrq2xPUWutHivDuOshZXqQ5rGbZM84HEaXKbLdItse7weMGOUdDVcLKEQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"prosemirror-state": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-commands": {
|
||||
"version": "1.7.1",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.7.1.tgz",
|
||||
"integrity": "sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"prosemirror-model": "^1.0.0",
|
||||
"prosemirror-state": "^1.0.0",
|
||||
"prosemirror-transform": "^1.10.2"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-dropcursor": {
|
||||
"version": "1.8.2",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-dropcursor/-/prosemirror-dropcursor-1.8.2.tgz",
|
||||
"integrity": "sha512-CCk6Gyx9+Tt2sbYk5NK0nB1ukHi2ryaRgadV/LvyNuO3ena1payM2z6Cg0vO1ebK8cxbzo41ku2DE5Axj1Zuiw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"prosemirror-state": "^1.0.0",
|
||||
"prosemirror-transform": "^1.1.0",
|
||||
"prosemirror-view": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-gapcursor": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-gapcursor/-/prosemirror-gapcursor-1.4.0.tgz",
|
||||
"integrity": "sha512-z00qvurSdCEWUIulij/isHaqu4uLS8r/Fi61IbjdIPJEonQgggbJsLnstW7Lgdk4zQ68/yr6B6bf7sJXowIgdQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"prosemirror-keymap": "^1.0.0",
|
||||
"prosemirror-model": "^1.0.0",
|
||||
"prosemirror-state": "^1.0.0",
|
||||
"prosemirror-view": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-history": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-history/-/prosemirror-history-1.5.0.tgz",
|
||||
"integrity": "sha512-zlzTiH01eKA55UAf1MEjtssJeHnGxO0j4K4Dpx+gnmX9n+SHNlDqI2oO1Kv1iPN5B1dm5fsljCfqKF9nFL6HRg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"prosemirror-state": "^1.2.2",
|
||||
"prosemirror-transform": "^1.0.0",
|
||||
"prosemirror-view": "^1.31.0",
|
||||
"rope-sequence": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-inputrules": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-inputrules/-/prosemirror-inputrules-1.5.1.tgz",
|
||||
"integrity": "sha512-7wj4uMjKaXWAQ1CDgxNzNtR9AlsuwzHfdFH1ygEHA2KHF2DOEaXl1CJfNPAKCg9qNEh4rum975QLaCiQPyY6Fw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"prosemirror-state": "^1.0.0",
|
||||
"prosemirror-transform": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-keymap": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-keymap/-/prosemirror-keymap-1.2.3.tgz",
|
||||
"integrity": "sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"prosemirror-state": "^1.0.0",
|
||||
"w3c-keyname": "^2.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-markdown": {
|
||||
"version": "1.13.2",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-markdown/-/prosemirror-markdown-1.13.2.tgz",
|
||||
"integrity": "sha512-FPD9rHPdA9fqzNmIIDhhnYQ6WgNoSWX9StUZ8LEKapaXU9i6XgykaHKhp6XMyXlOWetmaFgGDS/nu/w9/vUc5g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/markdown-it": "^14.0.0",
|
||||
"markdown-it": "^14.0.0",
|
||||
"prosemirror-model": "^1.25.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-menu": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-menu/-/prosemirror-menu-1.2.5.tgz",
|
||||
"integrity": "sha512-qwXzynnpBIeg1D7BAtjOusR+81xCp53j7iWu/IargiRZqRjGIlQuu1f3jFi+ehrHhWMLoyOQTSRx/IWZJqOYtQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"crelt": "^1.0.0",
|
||||
"prosemirror-commands": "^1.0.0",
|
||||
"prosemirror-history": "^1.0.0",
|
||||
"prosemirror-state": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-model": {
|
||||
"version": "1.25.4",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.4.tgz",
|
||||
"integrity": "sha512-PIM7E43PBxKce8OQeezAs9j4TP+5yDpZVbuurd1h5phUxEKIu+G2a+EUZzIC5nS1mJktDJWzbqS23n1tsAf5QA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"orderedmap": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-schema-basic": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-schema-basic/-/prosemirror-schema-basic-1.2.4.tgz",
|
||||
"integrity": "sha512-ELxP4TlX3yr2v5rM7Sb70SqStq5NvI15c0j9j/gjsrO5vaw+fnnpovCLEGIcpeGfifkuqJwl4fon6b+KdrODYQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"prosemirror-model": "^1.25.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-schema-list": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-schema-list/-/prosemirror-schema-list-1.5.1.tgz",
|
||||
"integrity": "sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"prosemirror-model": "^1.0.0",
|
||||
"prosemirror-state": "^1.0.0",
|
||||
"prosemirror-transform": "^1.7.3"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-state": {
|
||||
"version": "1.4.4",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.4.tgz",
|
||||
"integrity": "sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"prosemirror-model": "^1.0.0",
|
||||
"prosemirror-transform": "^1.0.0",
|
||||
"prosemirror-view": "^1.27.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-tables": {
|
||||
"version": "1.8.3",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.8.3.tgz",
|
||||
"integrity": "sha512-wbqCR/RlRPRe41a4LFtmhKElzBEfBTdtAYWNIGHM6X2e24NN/MTNUKyXjjphfAfdQce37Kh/5yf765mLPYDe7Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"prosemirror-keymap": "^1.2.3",
|
||||
"prosemirror-model": "^1.25.4",
|
||||
"prosemirror-state": "^1.4.4",
|
||||
"prosemirror-transform": "^1.10.5",
|
||||
"prosemirror-view": "^1.41.4"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-trailing-node": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-trailing-node/-/prosemirror-trailing-node-3.0.0.tgz",
|
||||
"integrity": "sha512-xiun5/3q0w5eRnGYfNlW1uU9W6x5MoFKWwq/0TIRgt09lv7Hcser2QYV8t4muXbEr+Fwo0geYn79Xs4GKywrRQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@remirror/core-constants": "3.0.0",
|
||||
"escape-string-regexp": "^4.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"prosemirror-model": "^1.22.1",
|
||||
"prosemirror-state": "^1.4.2",
|
||||
"prosemirror-view": "^1.33.8"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-transform": {
|
||||
"version": "1.10.5",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.10.5.tgz",
|
||||
"integrity": "sha512-RPDQCxIDhIBb1o36xxwsaeAvivO8VLJcgBtzmOwQ64bMtsVFh5SSuJ6dWSxO1UsHTiTXPCgQm3PDJt7p6IOLbw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"prosemirror-model": "^1.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-view": {
|
||||
"version": "1.41.4",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.41.4.tgz",
|
||||
"integrity": "sha512-WkKgnyjNncri03Gjaz3IFWvCAE94XoiEgvtr0/r2Xw7R8/IjK3sKLSiDoCHWcsXSAinVaKlGRZDvMCsF1kbzjA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"prosemirror-model": "^1.20.0",
|
||||
"prosemirror-state": "^1.0.0",
|
||||
"prosemirror-transform": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/punycode": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||
@@ -7791,6 +8569,15 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/punycode.js": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz",
|
||||
"integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/queue-microtask": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
||||
@@ -8115,6 +8902,12 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/rope-sequence": {
|
||||
"version": "1.3.4",
|
||||
"resolved": "https://registry.npmjs.org/rope-sequence/-/rope-sequence-1.3.4.tgz",
|
||||
"integrity": "sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/run-parallel": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
|
||||
@@ -8887,6 +9680,12 @@
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/uc.micro": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz",
|
||||
"integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/unbox-primitive": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz",
|
||||
@@ -9059,6 +9858,12 @@
|
||||
"d3-timer": "^3.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/w3c-keyname": {
|
||||
"version": "2.2.8",
|
||||
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
|
||||
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
|
||||
@@ -28,6 +28,14 @@
|
||||
"@radix-ui/react-switch": "^1.2.6",
|
||||
"@radix-ui/react-tabs": "^1.1.13",
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
"@tiptap/extension-image": "^3.13.0",
|
||||
"@tiptap/extension-link": "^3.13.0",
|
||||
"@tiptap/extension-placeholder": "^3.13.0",
|
||||
"@tiptap/extension-text-align": "^3.13.0",
|
||||
"@tiptap/extension-underline": "^3.13.0",
|
||||
"@tiptap/pm": "^3.13.0",
|
||||
"@tiptap/react": "^3.13.0",
|
||||
"@tiptap/starter-kit": "^3.13.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
@@ -49,6 +57,7 @@
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.57.0",
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@types/google.maps": "^3.58.1",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
'use client';
|
||||
|
||||
import { useParams } from 'next/navigation';
|
||||
import { BadDebtDetail } from '@/components/accounting/BadDebtCollection/BadDebtDetail';
|
||||
|
||||
export default function EditBadDebtPage() {
|
||||
const params = useParams();
|
||||
const recordId = params.id as string;
|
||||
|
||||
return <BadDebtDetail mode="edit" recordId={recordId} />;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
'use client';
|
||||
|
||||
import { useParams } from 'next/navigation';
|
||||
import { BadDebtDetail } from '@/components/accounting/BadDebtCollection/BadDebtDetail';
|
||||
|
||||
export default function BadDebtDetailPage() {
|
||||
const params = useParams();
|
||||
const recordId = params.id as string;
|
||||
|
||||
return <BadDebtDetail mode="view" recordId={recordId} />;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { BadDebtDetail } from '@/components/accounting/BadDebtCollection/BadDebtDetail';
|
||||
|
||||
export default function NewBadDebtPage() {
|
||||
return <BadDebtDetail mode="new" />;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { BadDebtCollection } from '@/components/accounting/BadDebtCollection';
|
||||
|
||||
export default function BadDebtCollectionPage() {
|
||||
return <BadDebtCollection />;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { BankTransactionInquiry } from '@/components/accounting/BankTransactionInquiry';
|
||||
|
||||
export default function BankTransactionsPage() {
|
||||
return <BankTransactionInquiry />;
|
||||
}
|
||||
13
src/app/[locale]/(protected)/accounting/bills/[id]/page.tsx
Normal file
13
src/app/[locale]/(protected)/accounting/bills/[id]/page.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
'use client';
|
||||
|
||||
import { useParams, useSearchParams } from 'next/navigation';
|
||||
import { BillDetail } from '@/components/accounting/BillManagement/BillDetail';
|
||||
|
||||
export default function BillDetailPage() {
|
||||
const params = useParams();
|
||||
const searchParams = useSearchParams();
|
||||
const billId = params.id as string;
|
||||
const mode = searchParams.get('mode') === 'edit' ? 'edit' : 'view';
|
||||
|
||||
return <BillDetail billId={billId} mode={mode} />;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { BillDetail } from '@/components/accounting/BillManagement/BillDetail';
|
||||
|
||||
export default function BillNewPage() {
|
||||
return <BillDetail billId="new" mode="new" />;
|
||||
}
|
||||
12
src/app/[locale]/(protected)/accounting/bills/page.tsx
Normal file
12
src/app/[locale]/(protected)/accounting/bills/page.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
'use client';
|
||||
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { BillManagement } from '@/components/accounting/BillManagement';
|
||||
|
||||
export default function BillsPage() {
|
||||
const searchParams = useSearchParams();
|
||||
const vendorId = searchParams.get('vendorId') || undefined;
|
||||
const billType = searchParams.get('type') || undefined;
|
||||
|
||||
return <BillManagement initialVendorId={vendorId} initialBillType={billType} />;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { CardTransactionInquiry } from '@/components/accounting/CardTransactionInquiry';
|
||||
|
||||
export default function CardTransactionsPage() {
|
||||
return <CardTransactionInquiry />;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { DailyReport } from '@/components/accounting/DailyReport';
|
||||
|
||||
export default function DailyReportPage() {
|
||||
return <DailyReport />;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
'use client';
|
||||
|
||||
import { useParams, useSearchParams } from 'next/navigation';
|
||||
import { DepositDetail } from '@/components/accounting/DepositManagement/DepositDetail';
|
||||
|
||||
export default function DepositDetailPage() {
|
||||
const params = useParams();
|
||||
const searchParams = useSearchParams();
|
||||
const depositId = params.id as string;
|
||||
const mode = searchParams.get('mode') === 'edit' ? 'edit' : 'view';
|
||||
|
||||
return <DepositDetail depositId={depositId} mode={mode} />;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { DepositManagement } from '@/components/accounting/DepositManagement';
|
||||
|
||||
export default function DepositsPage() {
|
||||
return <DepositManagement />;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { ExpectedExpenseManagement } from '@/components/accounting/ExpectedExpenseManagement';
|
||||
|
||||
export default function ExpectedExpensesPage() {
|
||||
return <ExpectedExpenseManagement />;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
'use client';
|
||||
|
||||
import { useParams, useSearchParams } from 'next/navigation';
|
||||
import { PurchaseDetail } from '@/components/accounting/PurchaseManagement/PurchaseDetail';
|
||||
|
||||
export default function PurchaseDetailPage() {
|
||||
const params = useParams();
|
||||
const searchParams = useSearchParams();
|
||||
const purchaseId = params.id as string;
|
||||
const mode = searchParams.get('mode') === 'edit' ? 'edit' : 'view';
|
||||
|
||||
return <PurchaseDetail purchaseId={purchaseId} mode={mode} />;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { PurchaseManagement } from '@/components/accounting/PurchaseManagement';
|
||||
|
||||
export default function PurchasePage() {
|
||||
return <PurchaseManagement />;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
'use client';
|
||||
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { ReceivablesStatus } from '@/components/accounting/ReceivablesStatus';
|
||||
|
||||
export default function ReceivablesStatusPage() {
|
||||
const searchParams = useSearchParams();
|
||||
const highlightVendorId = searchParams.get('highlight') || undefined;
|
||||
|
||||
return <ReceivablesStatus highlightVendorId={highlightVendorId} />;
|
||||
}
|
||||
13
src/app/[locale]/(protected)/accounting/sales/[id]/page.tsx
Normal file
13
src/app/[locale]/(protected)/accounting/sales/[id]/page.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
'use client';
|
||||
|
||||
import { useParams, useSearchParams } from 'next/navigation';
|
||||
import { SalesDetail } from '@/components/accounting/SalesManagement/SalesDetail';
|
||||
|
||||
export default function SalesDetailPage() {
|
||||
const params = useParams();
|
||||
const searchParams = useSearchParams();
|
||||
const salesId = params.id as string;
|
||||
const mode = searchParams.get('mode') === 'edit' ? 'edit' : 'view';
|
||||
|
||||
return <SalesDetail mode={mode} salesId={salesId} />;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { SalesDetail } from '@/components/accounting/SalesManagement/SalesDetail';
|
||||
|
||||
export default function NewSalesPage() {
|
||||
return <SalesDetail mode="new" />;
|
||||
}
|
||||
5
src/app/[locale]/(protected)/accounting/sales/page.tsx
Normal file
5
src/app/[locale]/(protected)/accounting/sales/page.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { SalesManagement } from '@/components/accounting/SalesManagement';
|
||||
|
||||
export default function SalesPage() {
|
||||
return <SalesManagement />;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
'use client';
|
||||
|
||||
import { useParams } from 'next/navigation';
|
||||
import { VendorLedgerDetail } from '@/components/accounting/VendorLedger/VendorLedgerDetail';
|
||||
|
||||
export default function VendorLedgerDetailPage() {
|
||||
const params = useParams();
|
||||
const vendorId = params.id as string;
|
||||
|
||||
return <VendorLedgerDetail vendorId={vendorId} />;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { VendorLedger } from '@/components/accounting/VendorLedger';
|
||||
|
||||
export default function VendorLedgerPage() {
|
||||
return <VendorLedger />;
|
||||
}
|
||||
13
src/app/[locale]/(protected)/accounting/vendors/[id]/page.tsx
vendored
Normal file
13
src/app/[locale]/(protected)/accounting/vendors/[id]/page.tsx
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
'use client';
|
||||
|
||||
import { useParams, useSearchParams } from 'next/navigation';
|
||||
import { VendorDetail } from '@/components/accounting/VendorManagement/VendorDetail';
|
||||
|
||||
export default function VendorDetailPage() {
|
||||
const params = useParams();
|
||||
const searchParams = useSearchParams();
|
||||
const vendorId = params.id as string;
|
||||
const mode = searchParams.get('mode') === 'edit' ? 'edit' : 'view';
|
||||
|
||||
return <VendorDetail mode={mode} vendorId={vendorId} />;
|
||||
}
|
||||
7
src/app/[locale]/(protected)/accounting/vendors/new/page.tsx
vendored
Normal file
7
src/app/[locale]/(protected)/accounting/vendors/new/page.tsx
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { VendorDetail } from '@/components/accounting/VendorManagement/VendorDetail';
|
||||
|
||||
export default function NewVendorPage() {
|
||||
return <VendorDetail mode="new" />;
|
||||
}
|
||||
7
src/app/[locale]/(protected)/accounting/vendors/page.tsx
vendored
Normal file
7
src/app/[locale]/(protected)/accounting/vendors/page.tsx
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { VendorManagement } from '@/components/accounting/VendorManagement';
|
||||
|
||||
export default function VendorsPage() {
|
||||
return <VendorManagement />;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
'use client';
|
||||
|
||||
import { useParams, useSearchParams } from 'next/navigation';
|
||||
import { WithdrawalDetail } from '@/components/accounting/WithdrawalManagement/WithdrawalDetail';
|
||||
|
||||
export default function WithdrawalDetailPage() {
|
||||
const params = useParams();
|
||||
const searchParams = useSearchParams();
|
||||
const withdrawalId = params.id as string;
|
||||
const mode = searchParams.get('mode') === 'edit' ? 'edit' : 'view';
|
||||
|
||||
return <WithdrawalDetail withdrawalId={withdrawalId} mode={mode} />;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { WithdrawalManagement } from '@/components/accounting/WithdrawalManagement';
|
||||
|
||||
export default function WithdrawalsPage() {
|
||||
return <WithdrawalManagement />;
|
||||
}
|
||||
57
src/app/[locale]/(protected)/board/[id]/edit/page.tsx
Normal file
57
src/app/[locale]/(protected)/board/[id]/edit/page.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* 게시글 수정 페이지
|
||||
*/
|
||||
|
||||
import { useParams } from 'next/navigation';
|
||||
import { useMemo } from 'react';
|
||||
import { format } from 'date-fns';
|
||||
import { BoardForm } from '@/components/board/BoardForm';
|
||||
import type { Post } from '@/components/board/types';
|
||||
import { MOCK_BOARDS } from '@/components/board/types';
|
||||
|
||||
// Mock 데이터 생성 (실제로는 API에서 가져옴)
|
||||
const generateMockPost = (id: string): Post => {
|
||||
const boards = MOCK_BOARDS.filter((b) => b.id !== 'all');
|
||||
const board = boards[0];
|
||||
|
||||
return {
|
||||
id,
|
||||
boardId: board.id,
|
||||
boardName: board.name,
|
||||
title: '제목',
|
||||
content: `
|
||||
<p>게시글 내용입니다.</p>
|
||||
<p>이것은 <strong>테스트용</strong> 콘텐츠입니다.</p>
|
||||
`,
|
||||
authorId: 'user1',
|
||||
authorName: '홍길동',
|
||||
authorDepartment: '개발팀',
|
||||
authorPosition: '과장',
|
||||
isPinned: false,
|
||||
allowComments: true,
|
||||
viewCount: 123,
|
||||
attachments: [
|
||||
{
|
||||
id: 'file-1',
|
||||
fileName: 'abc.pdf',
|
||||
fileSize: 1024000,
|
||||
fileUrl: '/files/abc.pdf',
|
||||
mimeType: 'application/pdf',
|
||||
},
|
||||
],
|
||||
createdAt: format(new Date(2025, 8, 9, 12, 20), "yyyy-MM-dd'T'HH:mm:ss"),
|
||||
updatedAt: format(new Date(2025, 8, 9, 12, 20), "yyyy-MM-dd'T'HH:mm:ss"),
|
||||
};
|
||||
};
|
||||
|
||||
export default function BoardEditPage() {
|
||||
const params = useParams();
|
||||
const postId = params.id as string;
|
||||
|
||||
// Mock 데이터 (실제로는 API에서 가져옴)
|
||||
const post = useMemo(() => generateMockPost(postId), [postId]);
|
||||
|
||||
return <BoardForm mode="edit" initialData={post} />;
|
||||
}
|
||||
94
src/app/[locale]/(protected)/board/[id]/page.tsx
Normal file
94
src/app/[locale]/(protected)/board/[id]/page.tsx
Normal file
@@ -0,0 +1,94 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* 게시글 상세 페이지
|
||||
*/
|
||||
|
||||
import { useParams } from 'next/navigation';
|
||||
import { useMemo } from 'react';
|
||||
import { format } from 'date-fns';
|
||||
import { BoardDetail } from '@/components/board/BoardDetail';
|
||||
import type { Post, Comment } from '@/components/board/types';
|
||||
import { MOCK_BOARDS } from '@/components/board/types';
|
||||
|
||||
// 현재 로그인 사용자 ID (실제로는 auth context에서 가져옴)
|
||||
const CURRENT_USER_ID = 'user1';
|
||||
|
||||
// Mock 데이터 생성 (실제로는 API에서 가져옴)
|
||||
const generateMockPost = (id: string): Post => {
|
||||
const boards = MOCK_BOARDS.filter((b) => b.id !== 'all');
|
||||
const board = boards[0];
|
||||
|
||||
return {
|
||||
id,
|
||||
boardId: board.id,
|
||||
boardName: board.name,
|
||||
title: '제목',
|
||||
content: `
|
||||
<p>게시글 내용입니다.</p>
|
||||
<p>이것은 <strong>테스트용</strong> 콘텐츠입니다.</p>
|
||||
<p><img src="https://via.placeholder.com/600x300" alt="IMG" /></p>
|
||||
<p>내용</p>
|
||||
`,
|
||||
authorId: 'user1',
|
||||
authorName: '홍길동',
|
||||
authorDepartment: '개발팀',
|
||||
authorPosition: '과장',
|
||||
isPinned: false,
|
||||
allowComments: true,
|
||||
viewCount: 123,
|
||||
attachments: [
|
||||
{
|
||||
id: 'file-1',
|
||||
fileName: 'abc.pdf',
|
||||
fileSize: 1024000,
|
||||
fileUrl: '/files/abc.pdf',
|
||||
mimeType: 'application/pdf',
|
||||
},
|
||||
],
|
||||
createdAt: format(new Date(2025, 8, 3, 12, 23), "yyyy-MM-dd'T'HH:mm:ss"),
|
||||
updatedAt: format(new Date(2025, 8, 3, 12, 23), "yyyy-MM-dd'T'HH:mm:ss"),
|
||||
};
|
||||
};
|
||||
|
||||
const generateMockComments = (postId: string): Comment[] => [
|
||||
{
|
||||
id: 'comment-1',
|
||||
postId,
|
||||
authorId: 'user2',
|
||||
authorName: '이름 직책',
|
||||
authorDepartment: '부서명',
|
||||
authorPosition: '',
|
||||
content: '댓글 내용',
|
||||
createdAt: format(new Date(2025, 8, 3, 15, 33), "yyyy-MM-dd'T'HH:mm:ss"),
|
||||
updatedAt: format(new Date(2025, 8, 3, 15, 33), "yyyy-MM-dd'T'HH:mm:ss"),
|
||||
},
|
||||
{
|
||||
id: 'comment-2',
|
||||
postId,
|
||||
authorId: 'user1', // 본인 댓글
|
||||
authorName: '이름 직책',
|
||||
authorDepartment: '부서명',
|
||||
authorPosition: '',
|
||||
content: '댓글 내용',
|
||||
createdAt: format(new Date(2025, 8, 3, 15, 33), "yyyy-MM-dd'T'HH:mm:ss"),
|
||||
updatedAt: format(new Date(2025, 8, 3, 15, 33), "yyyy-MM-dd'T'HH:mm:ss"),
|
||||
},
|
||||
];
|
||||
|
||||
export default function BoardDetailPage() {
|
||||
const params = useParams();
|
||||
const postId = params.id as string;
|
||||
|
||||
// Mock 데이터 (실제로는 API에서 가져옴)
|
||||
const post = useMemo(() => generateMockPost(postId), [postId]);
|
||||
const comments = useMemo(() => generateMockComments(postId), [postId]);
|
||||
|
||||
return (
|
||||
<BoardDetail
|
||||
post={post}
|
||||
comments={comments}
|
||||
currentUserId={CURRENT_USER_ID}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
'use client';
|
||||
|
||||
import { useRouter, useParams } from 'next/navigation';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { BoardForm } from '@/components/board/BoardManagement/BoardForm';
|
||||
import type { Board, BoardFormData } from '@/components/board/BoardManagement/types';
|
||||
|
||||
// TODO: 실제 API에서 데이터 가져오기
|
||||
const mockBoard: Board = {
|
||||
id: '1',
|
||||
target: 'all',
|
||||
boardName: '공지사항',
|
||||
status: 'active',
|
||||
authorId: 'u1',
|
||||
authorName: '홍길동',
|
||||
createdAt: '2025-09-09T12:20:00Z',
|
||||
updatedAt: '2025-09-09T12:20:00Z',
|
||||
};
|
||||
|
||||
export default function BoardEditPage() {
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
const [board, setBoard] = useState<Board | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
// TODO: API 연동
|
||||
// const id = params.id;
|
||||
setBoard(mockBoard);
|
||||
}, [params.id]);
|
||||
|
||||
const handleSubmit = (data: BoardFormData) => {
|
||||
// TODO: API 연동
|
||||
console.log('Update board:', params.id, data);
|
||||
router.push(`/ko/board/board-management/${params.id}`);
|
||||
};
|
||||
|
||||
if (!board) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="text-center">
|
||||
<div className="inline-block h-8 w-8 animate-spin rounded-full border-4 border-solid border-primary border-r-transparent mb-4"></div>
|
||||
<p className="text-muted-foreground">로딩 중...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<BoardForm
|
||||
mode="edit"
|
||||
board={board}
|
||||
onSubmit={handleSubmit}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
'use client';
|
||||
|
||||
import { useRouter, useParams } from 'next/navigation';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { BoardDetail } from '@/components/board/BoardManagement/BoardDetail';
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog';
|
||||
import type { Board } from '@/components/board/BoardManagement/types';
|
||||
|
||||
// TODO: 실제 API에서 데이터 가져오기
|
||||
const mockBoard: Board = {
|
||||
id: '1',
|
||||
target: 'all',
|
||||
boardName: '공지사항',
|
||||
status: 'active',
|
||||
authorId: 'u1',
|
||||
authorName: '홍길동',
|
||||
createdAt: '2025-09-09T12:20:00Z',
|
||||
updatedAt: '2025-09-09T12:20:00Z',
|
||||
};
|
||||
|
||||
export default function BoardDetailPage() {
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
const [board, setBoard] = useState<Board | null>(null);
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// TODO: API 연동
|
||||
// const id = params.id;
|
||||
setBoard(mockBoard);
|
||||
}, [params.id]);
|
||||
|
||||
const handleEdit = () => {
|
||||
router.push(`/ko/board/board-management/${params.id}/edit`);
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
setDeleteDialogOpen(true);
|
||||
};
|
||||
|
||||
const confirmDelete = () => {
|
||||
// TODO: API 연동
|
||||
console.log('Delete board:', params.id);
|
||||
router.push('/ko/board/board-management');
|
||||
};
|
||||
|
||||
if (!board) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="text-center">
|
||||
<div className="inline-block h-8 w-8 animate-spin rounded-full border-4 border-solid border-primary border-r-transparent mb-4"></div>
|
||||
<p className="text-muted-foreground">로딩 중...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<BoardDetail
|
||||
board={board}
|
||||
onEdit={handleEdit}
|
||||
onDelete={handleDelete}
|
||||
/>
|
||||
|
||||
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>게시판 삭제</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
"{board.boardName}" 게시판을 삭제하시겠습니까?
|
||||
<br />
|
||||
<span className="text-destructive">
|
||||
삭제된 게시판 정보는 복구할 수 없습니다.
|
||||
</span>
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>취소</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={confirmDelete}
|
||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||
>
|
||||
삭제
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
'use client';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { BoardForm } from '@/components/board/BoardManagement/BoardForm';
|
||||
import type { BoardFormData } from '@/components/board/BoardManagement/types';
|
||||
|
||||
export default function BoardNewPage() {
|
||||
const router = useRouter();
|
||||
|
||||
const handleSubmit = (data: BoardFormData) => {
|
||||
// TODO: API 연동
|
||||
console.log('Create board:', data);
|
||||
router.push('/ko/board/board-management');
|
||||
};
|
||||
|
||||
return (
|
||||
<BoardForm
|
||||
mode="create"
|
||||
onSubmit={handleSubmit}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { BoardManagement } from '@/components/board/BoardManagement';
|
||||
|
||||
export default function BoardManagementPage() {
|
||||
return <BoardManagement />;
|
||||
}
|
||||
14
src/app/[locale]/(protected)/board/create/page.tsx
Normal file
14
src/app/[locale]/(protected)/board/create/page.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* 게시글 등록 페이지
|
||||
*/
|
||||
|
||||
import { BoardForm } from '@/components/board/BoardForm';
|
||||
|
||||
export default function BoardCreatePage() {
|
||||
return <BoardForm mode="create" />;
|
||||
}
|
||||
|
||||
export const metadata = {
|
||||
title: '게시글 등록',
|
||||
description: '게시글을 등록하고 관리합니다.',
|
||||
};
|
||||
14
src/app/[locale]/(protected)/board/page.tsx
Normal file
14
src/app/[locale]/(protected)/board/page.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* 게시판 목록 페이지
|
||||
*/
|
||||
|
||||
import { BoardList } from '@/components/board/BoardList';
|
||||
|
||||
export default function BoardPage() {
|
||||
return <BoardList />;
|
||||
}
|
||||
|
||||
export const metadata = {
|
||||
title: '게시판',
|
||||
description: '게시판의 게시글을 등록하고 관리합니다.',
|
||||
};
|
||||
5
src/app/[locale]/(protected)/company-info/page.tsx
Normal file
5
src/app/[locale]/(protected)/company-info/page.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { CompanyInfoManagement } from '@/components/settings/CompanyInfoManagement';
|
||||
|
||||
export default function CompanyInfoPage() {
|
||||
return <CompanyInfoManagement />;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
'use client';
|
||||
|
||||
import { useParams } from 'next/navigation';
|
||||
import { EventDetail, MOCK_EVENTS } from '@/components/customer-center/EventManagement';
|
||||
|
||||
export default function EventDetailPage() {
|
||||
const params = useParams();
|
||||
const eventId = params.id as string;
|
||||
|
||||
// Mock 데이터에서 이벤트 찾기
|
||||
const event = MOCK_EVENTS.find((e) => e.id === eventId);
|
||||
|
||||
if (!event) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<p className="text-muted-foreground">이벤트를 찾을 수 없습니다.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <EventDetail event={event} />;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { EventList } from '@/components/customer-center/EventManagement';
|
||||
|
||||
export default function EventsPage() {
|
||||
return <EventList />;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { FAQList } from '@/components/customer-center/FAQManagement';
|
||||
|
||||
export default function FAQPage() {
|
||||
return <FAQList />;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* 1:1 문의 수정 페이지
|
||||
*/
|
||||
|
||||
import { useParams } from 'next/navigation';
|
||||
import { InquiryForm } from '@/components/customer-center/InquiryManagement';
|
||||
import { MOCK_INQUIRIES } from '@/components/customer-center/InquiryManagement/types';
|
||||
|
||||
export default function InquiryEditPage() {
|
||||
const params = useParams();
|
||||
const inquiryId = params.id as string;
|
||||
|
||||
// Mock: 문의 데이터 조회
|
||||
const inquiry = MOCK_INQUIRIES.find((i) => i.id === inquiryId);
|
||||
|
||||
if (!inquiry) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<p className="text-muted-foreground">문의를 찾을 수 없습니다.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <InquiryForm mode="edit" initialData={inquiry} />;
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* 1:1 문의 상세 페이지
|
||||
*/
|
||||
|
||||
import { useParams } from 'next/navigation';
|
||||
import { InquiryDetail } from '@/components/customer-center/InquiryManagement';
|
||||
import { MOCK_INQUIRIES, MOCK_REPLY, MOCK_COMMENTS } from '@/components/customer-center/InquiryManagement/types';
|
||||
|
||||
export default function InquiryDetailPage() {
|
||||
const params = useParams();
|
||||
const inquiryId = params.id as string;
|
||||
|
||||
// Mock: 문의 데이터 조회
|
||||
const inquiry = MOCK_INQUIRIES.find((i) => i.id === inquiryId);
|
||||
|
||||
if (!inquiry) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<p className="text-muted-foreground">문의를 찾을 수 없습니다.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Mock: 답변 데이터 (답변완료 상태일 때만)
|
||||
const reply = inquiry.status === 'completed' ? MOCK_REPLY : undefined;
|
||||
|
||||
// Mock: 댓글 데이터
|
||||
const comments = MOCK_COMMENTS.filter((c) => c.inquiryId === inquiryId);
|
||||
|
||||
// Mock: 현재 사용자 ID
|
||||
const currentUserId = 'user1';
|
||||
|
||||
return (
|
||||
<InquiryDetail
|
||||
inquiry={inquiry}
|
||||
reply={reply}
|
||||
comments={comments}
|
||||
currentUserId={currentUserId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* 1:1 문의 등록 페이지
|
||||
*/
|
||||
|
||||
import { InquiryForm } from '@/components/customer-center/InquiryManagement';
|
||||
|
||||
export default function InquiryCreatePage() {
|
||||
return <InquiryForm mode="create" />;
|
||||
}
|
||||
|
||||
export const metadata = {
|
||||
title: '1:1 문의 등록',
|
||||
description: '1:1 문의를 등록합니다.',
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* 1:1 문의 목록 페이지
|
||||
*/
|
||||
|
||||
import { InquiryList } from '@/components/customer-center/InquiryManagement';
|
||||
|
||||
export default function InquiriesPage() {
|
||||
return <InquiryList />;
|
||||
}
|
||||
|
||||
export const metadata = {
|
||||
title: '1:1 문의',
|
||||
description: '1:1 문의를 등록하고 답변을 확인합니다.',
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
'use client';
|
||||
|
||||
import { useParams } from 'next/navigation';
|
||||
import { NoticeDetail, MOCK_NOTICES } from '@/components/customer-center/NoticeManagement';
|
||||
|
||||
export default function NoticeDetailPage() {
|
||||
const params = useParams();
|
||||
const id = params.id as string;
|
||||
|
||||
// Mock 데이터에서 해당 공지사항 찾기
|
||||
const notice = MOCK_NOTICES.find((n) => n.id === id);
|
||||
|
||||
if (!notice) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<p className="text-muted-foreground">공지사항을 찾을 수 없습니다.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <NoticeDetail notice={notice} />;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { NoticeList } from '@/components/customer-center/NoticeManagement';
|
||||
|
||||
export default function NoticesPage() {
|
||||
return <NoticeList />;
|
||||
}
|
||||
267
src/app/[locale]/(protected)/dev/test-urls/TestUrlsClient.tsx
Normal file
267
src/app/[locale]/(protected)/dev/test-urls/TestUrlsClient.tsx
Normal file
@@ -0,0 +1,267 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { ExternalLink, Copy, Check, ChevronDown, ChevronRight, RefreshCw } from 'lucide-react';
|
||||
|
||||
export interface UrlItem {
|
||||
name: string;
|
||||
url: string;
|
||||
status?: string;
|
||||
}
|
||||
|
||||
export interface UrlCategory {
|
||||
title: string;
|
||||
icon: string;
|
||||
items: UrlItem[];
|
||||
subCategories?: {
|
||||
title: string;
|
||||
items: UrlItem[];
|
||||
}[];
|
||||
}
|
||||
|
||||
interface TestUrlsClientProps {
|
||||
initialData: UrlCategory[];
|
||||
lastUpdated: string;
|
||||
}
|
||||
|
||||
function UrlCard({ item, baseUrl }: { item: UrlItem; baseUrl: string }) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
const fullUrl = `${baseUrl}${item.url}`;
|
||||
|
||||
const handleCopy = async (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
await navigator.clipboard.writeText(fullUrl);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
};
|
||||
|
||||
const handleOpen = () => {
|
||||
window.open(fullUrl, '_blank');
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={handleOpen}
|
||||
className="group flex items-center justify-between p-3 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 hover:border-blue-400 dark:hover:border-blue-500 hover:shadow-md transition-all cursor-pointer"
|
||||
>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium text-gray-900 dark:text-white truncate">
|
||||
{item.name}
|
||||
</span>
|
||||
{item.status && (
|
||||
<span className="text-xs px-1.5 py-0.5 rounded bg-gray-100 dark:bg-gray-700">
|
||||
{item.status}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 truncate mt-0.5">
|
||||
{item.url}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 ml-2">
|
||||
<button
|
||||
onClick={handleCopy}
|
||||
className="p-1.5 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded transition-colors"
|
||||
title="URL 복사"
|
||||
>
|
||||
{copied ? <Check className="w-4 h-4 text-green-500" /> : <Copy className="w-4 h-4" />}
|
||||
</button>
|
||||
<ExternalLink className="w-4 h-4 text-gray-400 group-hover:text-blue-500 transition-colors" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CategorySection({ category, baseUrl }: { category: UrlCategory; baseUrl: string }) {
|
||||
const [expanded, setExpanded] = useState(true);
|
||||
const [subExpanded, setSubExpanded] = useState<Record<string, boolean>>({});
|
||||
|
||||
const toggleSub = (title: string) => {
|
||||
setSubExpanded((prev) => ({ ...prev, [title]: !prev[title] }));
|
||||
};
|
||||
|
||||
const totalItems = category.items.length +
|
||||
(category.subCategories?.reduce((acc, sub) => acc + sub.items.length, 0) || 0);
|
||||
|
||||
if (totalItems === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="mb-6">
|
||||
<button
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
className="flex items-center gap-2 w-full text-left mb-3"
|
||||
>
|
||||
{expanded ? (
|
||||
<ChevronDown className="w-5 h-5 text-gray-500" />
|
||||
) : (
|
||||
<ChevronRight className="w-5 h-5 text-gray-500" />
|
||||
)}
|
||||
<span className="text-xl">{category.icon}</span>
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
{category.title}
|
||||
</h2>
|
||||
<span className="text-sm text-gray-500 ml-auto">
|
||||
{totalItems} 링크
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{expanded && (
|
||||
<div className="pl-7 space-y-4">
|
||||
{category.items.length > 0 && (
|
||||
<div className="grid gap-2">
|
||||
{category.items.map((item) => (
|
||||
<UrlCard key={item.url} item={item} baseUrl={baseUrl} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{category.subCategories?.map((sub) => (
|
||||
<div key={sub.title} className="mt-3">
|
||||
<button
|
||||
onClick={() => toggleSub(sub.title)}
|
||||
className="flex items-center gap-2 text-sm text-gray-600 dark:text-gray-400 mb-2 hover:text-gray-900 dark:hover:text-white"
|
||||
>
|
||||
{subExpanded[sub.title] !== false ? (
|
||||
<ChevronDown className="w-4 h-4" />
|
||||
) : (
|
||||
<ChevronRight className="w-4 h-4" />
|
||||
)}
|
||||
{sub.title}
|
||||
<span className="text-xs text-gray-400">({sub.items.length})</span>
|
||||
</button>
|
||||
{subExpanded[sub.title] !== false && (
|
||||
<div className="grid gap-2 pl-6">
|
||||
{sub.items.map((item) => (
|
||||
<UrlCard key={item.url} item={item} baseUrl={baseUrl} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function TestUrlsClient({ initialData, lastUpdated }: TestUrlsClientProps) {
|
||||
const [baseUrl, setBaseUrl] = useState('http://localhost:3000');
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
setBaseUrl(window.location.origin);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 검색 필터링
|
||||
const filteredData = initialData
|
||||
.map((category) => ({
|
||||
...category,
|
||||
items: category.items.filter(
|
||||
(item) =>
|
||||
item.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
item.url.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
),
|
||||
subCategories: category.subCategories?.map((sub) => ({
|
||||
...sub,
|
||||
items: sub.items.filter(
|
||||
(item) =>
|
||||
item.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
item.url.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
),
|
||||
})).filter((sub) => sub.items.length > 0),
|
||||
}))
|
||||
.filter(
|
||||
(category) =>
|
||||
category.items.length > 0 || (category.subCategories && category.subCategories.length > 0)
|
||||
);
|
||||
|
||||
const totalLinks = initialData.reduce(
|
||||
(acc, cat) =>
|
||||
acc +
|
||||
cat.items.length +
|
||||
(cat.subCategories?.reduce((subAcc, sub) => subAcc + sub.items.length, 0) || 0),
|
||||
0
|
||||
);
|
||||
|
||||
const handleRefresh = () => {
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 p-6">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-white mb-2">
|
||||
🔗 테스트 URL 목록
|
||||
</h1>
|
||||
<button
|
||||
onClick={handleRefresh}
|
||||
className="flex items-center gap-2 px-3 py-1.5 text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg transition-colors"
|
||||
title="새로고침 (md 파일 변경사항 반영)"
|
||||
>
|
||||
<RefreshCw className="w-4 h-4" />
|
||||
새로고침
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-gray-600 dark:text-gray-400">
|
||||
백엔드 메뉴 연동 전 테스트용 직접 접근 URL ({totalLinks}개)
|
||||
</p>
|
||||
<p className="text-xs text-gray-400 mt-1">
|
||||
클릭하면 새 탭에서 열립니다 • 최종 업데이트: {lastUpdated}
|
||||
</p>
|
||||
<p className="text-xs text-green-600 dark:text-green-400 mt-1">
|
||||
✨ md 파일 수정 시 자동 반영됩니다
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Search & Base URL */}
|
||||
<div className="flex gap-4 mb-6">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="페이지 또는 URL 검색..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="flex-1 px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
/>
|
||||
<div className="flex items-center gap-2 px-3 py-2 bg-white dark:bg-gray-800 rounded-lg border border-gray-300 dark:border-gray-600">
|
||||
<span className="text-sm text-gray-500">Base:</span>
|
||||
<input
|
||||
type="text"
|
||||
value={baseUrl}
|
||||
onChange={(e) => setBaseUrl(e.target.value)}
|
||||
className="w-48 text-sm bg-transparent text-gray-900 dark:text-white focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Categories */}
|
||||
<div className="space-y-2">
|
||||
{filteredData.map((category) => (
|
||||
<CategorySection key={category.title} category={category} baseUrl={baseUrl} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
{filteredData.length === 0 && (
|
||||
<div className="text-center py-12 text-gray-500">
|
||||
검색 결과가 없습니다.
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Footer */}
|
||||
<div className="mt-8 pt-6 border-t border-gray-200 dark:border-gray-700 text-center text-sm text-gray-500">
|
||||
<p>
|
||||
📁 데이터 소스: <code className="bg-gray-100 dark:bg-gray-800 px-1.5 py-0.5 rounded">claudedocs/[REF] all-pages-test-urls.md</code>
|
||||
</p>
|
||||
<p className="mt-1 text-green-600 dark:text-green-400">
|
||||
md 파일 수정 후 새로고침하면 자동 반영!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
167
src/app/[locale]/(protected)/dev/test-urls/page.tsx
Normal file
167
src/app/[locale]/(protected)/dev/test-urls/page.tsx
Normal file
@@ -0,0 +1,167 @@
|
||||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
import TestUrlsClient, { UrlCategory, UrlItem } from './TestUrlsClient';
|
||||
|
||||
// 아이콘 매핑
|
||||
const iconMap: Record<string, string> = {
|
||||
'기본': '🏠',
|
||||
'인사관리': '👥',
|
||||
'HR': '👥',
|
||||
'판매관리': '💰',
|
||||
'Sales': '💰',
|
||||
'기준정보관리': '📦',
|
||||
'Master Data': '📦',
|
||||
'생산관리': '🏭',
|
||||
'Production': '🏭',
|
||||
'설정': '⚙️',
|
||||
'Settings': '⚙️',
|
||||
'전자결재': '📝',
|
||||
'Approval': '📝',
|
||||
'회계관리': '💵',
|
||||
'Accounting': '💵',
|
||||
'게시판': '📋',
|
||||
'Board': '📋',
|
||||
'보고서': '📊',
|
||||
'Reports': '📊',
|
||||
};
|
||||
|
||||
function getIcon(title: string): string {
|
||||
for (const [key, icon] of Object.entries(iconMap)) {
|
||||
if (title.includes(key)) return icon;
|
||||
}
|
||||
return '📄';
|
||||
}
|
||||
|
||||
function parseTableRow(line: string): UrlItem | null {
|
||||
// | 페이지 | URL | 상태 | 형식 파싱
|
||||
const parts = line.split('|').map(p => p.trim()).filter(p => p);
|
||||
|
||||
if (parts.length < 2) return null;
|
||||
if (parts[0] === '페이지' || parts[0].startsWith('---')) return null;
|
||||
|
||||
const name = parts[0].replace(/\*\*/g, ''); // **bold** 제거
|
||||
const url = parts[1].replace(/`/g, ''); // backtick 제거
|
||||
const status = parts[2] || undefined;
|
||||
|
||||
// URL이 /ko로 시작하는지 확인
|
||||
if (!url.startsWith('/ko')) return null;
|
||||
|
||||
return { name, url, status };
|
||||
}
|
||||
|
||||
function parseMdFile(content: string): { categories: UrlCategory[]; lastUpdated: string } {
|
||||
const lines = content.split('\n');
|
||||
const categories: UrlCategory[] = [];
|
||||
let currentCategory: UrlCategory | null = null;
|
||||
let currentSubCategory: { title: string; items: UrlItem[] } | null = null;
|
||||
let lastUpdated = 'N/A';
|
||||
|
||||
// Last Updated 추출
|
||||
const updateMatch = content.match(/Last Updated:\s*(\d{4}-\d{2}-\d{2})/);
|
||||
if (updateMatch) {
|
||||
lastUpdated = updateMatch[1];
|
||||
}
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i].trim();
|
||||
|
||||
// ## 카테고리 (메인 섹션)
|
||||
if (line.startsWith('## ') && !line.includes('클릭 가능한') && !line.includes('전체 URL') && !line.includes('백엔드 메뉴')) {
|
||||
// 이전 카테고리 저장
|
||||
if (currentCategory) {
|
||||
if (currentSubCategory) {
|
||||
currentCategory.subCategories = currentCategory.subCategories || [];
|
||||
currentCategory.subCategories.push(currentSubCategory);
|
||||
currentSubCategory = null;
|
||||
}
|
||||
categories.push(currentCategory);
|
||||
}
|
||||
|
||||
const title = line.replace('## ', '').replace(/[🏠👥💰📦🏭⚙️📝📋💵]/g, '').trim();
|
||||
currentCategory = {
|
||||
title,
|
||||
icon: getIcon(title),
|
||||
items: [],
|
||||
subCategories: [],
|
||||
};
|
||||
currentSubCategory = null;
|
||||
}
|
||||
|
||||
// ### 서브 카테고리
|
||||
else if (line.startsWith('### ') && currentCategory) {
|
||||
// 이전 서브카테고리 저장
|
||||
if (currentSubCategory) {
|
||||
currentCategory.subCategories = currentCategory.subCategories || [];
|
||||
currentCategory.subCategories.push(currentSubCategory);
|
||||
}
|
||||
|
||||
const subTitle = line.replace('### ', '').trim();
|
||||
// "메인 페이지"는 서브카테고리가 아니라 메인 아이템으로
|
||||
if (subTitle === '메인 페이지') {
|
||||
currentSubCategory = null;
|
||||
} else {
|
||||
currentSubCategory = {
|
||||
title: subTitle,
|
||||
items: [],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 테이블 행 파싱
|
||||
else if (line.startsWith('|') && currentCategory) {
|
||||
const item = parseTableRow(line);
|
||||
if (item) {
|
||||
if (currentSubCategory) {
|
||||
currentSubCategory.items.push(item);
|
||||
} else {
|
||||
currentCategory.items.push(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 마지막 카테고리 저장
|
||||
if (currentCategory) {
|
||||
if (currentSubCategory) {
|
||||
currentCategory.subCategories = currentCategory.subCategories || [];
|
||||
currentCategory.subCategories.push(currentSubCategory);
|
||||
}
|
||||
categories.push(currentCategory);
|
||||
}
|
||||
|
||||
// 빈 서브카테고리 제거
|
||||
categories.forEach(cat => {
|
||||
cat.subCategories = cat.subCategories?.filter(sub => sub.items.length > 0);
|
||||
});
|
||||
|
||||
return { categories, lastUpdated };
|
||||
}
|
||||
|
||||
export default async function TestUrlsPage() {
|
||||
// md 파일 경로
|
||||
const mdFilePath = path.join(
|
||||
process.cwd(),
|
||||
'claudedocs',
|
||||
'[REF] all-pages-test-urls.md'
|
||||
);
|
||||
|
||||
let urlData: UrlCategory[] = [];
|
||||
let lastUpdated = 'N/A';
|
||||
|
||||
try {
|
||||
const fileContent = await fs.readFile(mdFilePath, 'utf-8');
|
||||
const parsed = parseMdFile(fileContent);
|
||||
urlData = parsed.categories;
|
||||
lastUpdated = parsed.lastUpdated;
|
||||
} catch (error) {
|
||||
console.error('Failed to read md file:', error);
|
||||
// 파일 읽기 실패 시 빈 데이터
|
||||
urlData = [];
|
||||
}
|
||||
|
||||
return <TestUrlsClient initialData={urlData} lastUpdated={lastUpdated} />;
|
||||
}
|
||||
|
||||
// 캐싱 비활성화 - 항상 최신 md 파일 읽기
|
||||
export const dynamic = 'force-dynamic';
|
||||
export const revalidate = 0;
|
||||
295
src/app/[locale]/(protected)/hr/attendance/page.tsx
Normal file
295
src/app/[locale]/(protected)/hr/attendance/page.tsx
Normal file
@@ -0,0 +1,295 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { MapPin } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import GoogleMap from '@/components/attendance/GoogleMap';
|
||||
import AttendanceComplete from '@/components/attendance/AttendanceComplete';
|
||||
|
||||
// ========================================
|
||||
// 하드코딩 설정값 (MVP - 추후 API로 대체)
|
||||
// ========================================
|
||||
// TODO: 이 값들은 출퇴근관리 설정 페이지에서 관리됨
|
||||
// 설정 페이지 경로: /settings/attendance-settings
|
||||
// API 연동 시: GET /api/settings/attendance 에서 조회
|
||||
// ────────────────────────────────────────
|
||||
// - radius: 출퇴근관리 설정의 allowedRadius 값 사용
|
||||
// - gpsDepartments: 로그인 사용자의 부서가 포함되어 있는지 체크
|
||||
// - gpsEnabled: false면 GPS 출퇴근 기능 비활성화
|
||||
// ────────────────────────────────────────
|
||||
const SITE_LOCATION = {
|
||||
name: '본사',
|
||||
lat: 37.557358,
|
||||
lng: 126.864414,
|
||||
radius: 100, // meters → 출퇴근관리 설정의 allowedRadius 값으로 대체 예정
|
||||
};
|
||||
|
||||
const TEST_USER = {
|
||||
name: '홍길동',
|
||||
department: '부서명',
|
||||
position: '직급명',
|
||||
};
|
||||
|
||||
// 출퇴근 상태 타입
|
||||
type AttendanceStatus = 'not-checked-in' | 'checked-in' | 'checked-out';
|
||||
type ViewMode = 'main' | 'check-in-complete' | 'check-out-complete';
|
||||
|
||||
export default function MobileAttendancePage() {
|
||||
// Hydration 에러 방지: 클라이언트 마운트 상태
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
// 상태 관리
|
||||
const [currentTime, setCurrentTime] = useState<string>('--:--:--');
|
||||
const [currentDate, setCurrentDate] = useState<string>('');
|
||||
const [distance, setDistance] = useState<number | null>(null);
|
||||
const [isInRange, setIsInRange] = useState(false);
|
||||
const [attendanceStatus, setAttendanceStatus] = useState<AttendanceStatus>('not-checked-in');
|
||||
const [viewMode, setViewMode] = useState<ViewMode>('main');
|
||||
const [checkInTime, setCheckInTime] = useState<string>('');
|
||||
const [checkOutTime, setCheckOutTime] = useState<string>('');
|
||||
const [userName, setUserName] = useState(TEST_USER.name);
|
||||
const [userDepartment, setUserDepartment] = useState(TEST_USER.department);
|
||||
const [userPosition, setUserPosition] = useState(TEST_USER.position);
|
||||
|
||||
// 클라이언트 마운트 확인
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
// 현재 시간 업데이트 (마운트 후에만 실행)
|
||||
useEffect(() => {
|
||||
if (!mounted) return;
|
||||
|
||||
const updateTime = () => {
|
||||
const now = new Date();
|
||||
const hours = String(now.getHours()).padStart(2, '0');
|
||||
const minutes = String(now.getMinutes()).padStart(2, '0');
|
||||
const seconds = String(now.getSeconds()).padStart(2, '0');
|
||||
setCurrentTime(`${hours}:${minutes}:${seconds}`);
|
||||
|
||||
// 날짜 포맷
|
||||
const year = now.getFullYear();
|
||||
const month = now.getMonth() + 1;
|
||||
const date = now.getDate();
|
||||
const dayNames = ['일', '월', '화', '수', '목', '금', '토'];
|
||||
const day = dayNames[now.getDay()];
|
||||
setCurrentDate(`${year}년 ${month}월 ${date}일 (${day})`);
|
||||
};
|
||||
|
||||
updateTime();
|
||||
const interval = setInterval(updateTime, 1000);
|
||||
return () => clearInterval(interval);
|
||||
}, [mounted]);
|
||||
|
||||
// localStorage에서 사용자 정보 가져오기 (마운트 후에만 실행)
|
||||
useEffect(() => {
|
||||
if (!mounted) return;
|
||||
|
||||
const userDataStr = localStorage.getItem('user');
|
||||
if (userDataStr) {
|
||||
try {
|
||||
const userData = JSON.parse(userDataStr);
|
||||
if (userData.name) setUserName(userData.name);
|
||||
if (userData.department) setUserDepartment(userData.department);
|
||||
if (userData.position) setUserPosition(userData.position);
|
||||
} catch (e) {
|
||||
console.error('사용자 정보 파싱 실패:', e);
|
||||
}
|
||||
}
|
||||
}, [mounted]);
|
||||
|
||||
// 거리 변경 콜백
|
||||
const handleDistanceChange = useCallback((dist: number, inRange: boolean) => {
|
||||
setDistance(dist);
|
||||
setIsInRange(inRange);
|
||||
}, []);
|
||||
|
||||
// 출근하기
|
||||
const handleCheckIn = () => {
|
||||
if (!isInRange) return;
|
||||
|
||||
const now = new Date();
|
||||
const hours = String(now.getHours()).padStart(2, '0');
|
||||
const minutes = String(now.getMinutes()).padStart(2, '0');
|
||||
const seconds = String(now.getSeconds()).padStart(2, '0');
|
||||
const timeStr = `${hours}:${minutes}:${seconds}`;
|
||||
|
||||
setCheckInTime(timeStr);
|
||||
setAttendanceStatus('checked-in');
|
||||
setViewMode('check-in-complete');
|
||||
|
||||
// TODO: API 호출로 출근 기록 저장
|
||||
console.log('[출근 기록]', {
|
||||
time: timeStr,
|
||||
location: SITE_LOCATION.name,
|
||||
coordinates: { lat: SITE_LOCATION.lat, lng: SITE_LOCATION.lng },
|
||||
});
|
||||
};
|
||||
|
||||
// 퇴근하기
|
||||
const handleCheckOut = () => {
|
||||
if (!isInRange) return;
|
||||
|
||||
const now = new Date();
|
||||
const hours = String(now.getHours()).padStart(2, '0');
|
||||
const minutes = String(now.getMinutes()).padStart(2, '0');
|
||||
const seconds = String(now.getSeconds()).padStart(2, '0');
|
||||
const timeStr = `${hours}:${minutes}:${seconds}`;
|
||||
|
||||
setCheckOutTime(timeStr);
|
||||
setAttendanceStatus('checked-out');
|
||||
setViewMode('check-out-complete');
|
||||
|
||||
// TODO: API 호출로 퇴근 기록 저장
|
||||
console.log('[퇴근 기록]', {
|
||||
time: timeStr,
|
||||
location: SITE_LOCATION.name,
|
||||
coordinates: { lat: SITE_LOCATION.lat, lng: SITE_LOCATION.lng },
|
||||
});
|
||||
};
|
||||
|
||||
// 완료 화면에서 확인 클릭
|
||||
const handleConfirm = () => {
|
||||
setViewMode('main');
|
||||
};
|
||||
|
||||
// 마운트 전 로딩 UI (Hydration 에러 방지)
|
||||
if (!mounted) {
|
||||
return (
|
||||
<div className="flex flex-col h-[calc(100vh-100px)]">
|
||||
<div className="text-center py-3 border-b bg-white">
|
||||
<h1 className="text-lg font-semibold">출퇴근하기</h1>
|
||||
</div>
|
||||
<div className="flex-1 flex items-center justify-center bg-gray-100">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500 mx-auto mb-2"></div>
|
||||
<p className="text-gray-500 text-sm">로딩 중...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 완료 화면 렌더링
|
||||
if (viewMode === 'check-in-complete') {
|
||||
return (
|
||||
<div className="h-[calc(100vh-100px)]">
|
||||
<AttendanceComplete
|
||||
type="check-in"
|
||||
time={checkInTime}
|
||||
date={currentDate}
|
||||
location={SITE_LOCATION.name}
|
||||
onConfirm={handleConfirm}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (viewMode === 'check-out-complete') {
|
||||
return (
|
||||
<div className="h-[calc(100vh-100px)]">
|
||||
<AttendanceComplete
|
||||
type="check-out"
|
||||
time={checkOutTime}
|
||||
date={currentDate}
|
||||
location={SITE_LOCATION.name}
|
||||
onConfirm={handleConfirm}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 버튼 활성화 상태
|
||||
const canCheckIn = isInRange && attendanceStatus === 'not-checked-in';
|
||||
const canCheckOut = isInRange && attendanceStatus === 'checked-in';
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-[calc(100vh-100px)]">
|
||||
{/* 타이틀 */}
|
||||
<div className="text-center py-3 border-b bg-white">
|
||||
<h1 className="text-lg font-semibold">출퇴근하기</h1>
|
||||
</div>
|
||||
|
||||
{/* 지도 영역 */}
|
||||
<div className="flex-1 relative">
|
||||
<GoogleMap
|
||||
siteLocation={SITE_LOCATION}
|
||||
onDistanceChange={handleDistanceChange}
|
||||
/>
|
||||
|
||||
{/* 거리 표시 오버레이 */}
|
||||
{distance !== null && (
|
||||
<div className="absolute top-3 left-3 bg-white/90 backdrop-blur px-3 py-1.5 rounded-lg shadow-md">
|
||||
<div className="flex items-center gap-1.5 text-sm">
|
||||
<MapPin className="w-4 h-4 text-blue-500" />
|
||||
<span className={isInRange ? 'text-green-600 font-medium' : 'text-gray-600'}>
|
||||
{distance < 1000
|
||||
? `${Math.round(distance)}m`
|
||||
: `${(distance / 1000).toFixed(1)}km`}
|
||||
{isInRange && ' (범위 내)'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 사용자 정보 + 시간 + 버튼 */}
|
||||
<div className="bg-white border-t p-4 space-y-4">
|
||||
{/* 사용자 정보 */}
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 bg-gray-200 rounded-full flex items-center justify-center">
|
||||
<span className="text-gray-600 font-medium">{userName.charAt(0)}</span>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-semibold text-gray-900">{userName}</p>
|
||||
<p className="text-sm text-gray-500">
|
||||
{userDepartment} {userPosition}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 현재 시간 */}
|
||||
<div className="text-center">
|
||||
<p className="text-3xl font-bold text-red-500" suppressHydrationWarning>{currentTime}</p>
|
||||
<p className="text-xs text-gray-400 mt-0.5" suppressHydrationWarning>{currentDate}</p>
|
||||
{attendanceStatus === 'checked-in' && (
|
||||
<p className="text-sm text-green-600 mt-1">출근중</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 출근/퇴근 버튼 */}
|
||||
<div className="flex gap-3">
|
||||
<Button
|
||||
onClick={handleCheckIn}
|
||||
disabled={!canCheckIn}
|
||||
className={`flex-1 h-12 text-base font-medium rounded-lg transition-all ${
|
||||
canCheckIn
|
||||
? 'bg-blue-500 hover:bg-blue-600 text-white'
|
||||
: 'bg-gray-200 text-gray-400 cursor-not-allowed'
|
||||
}`}
|
||||
>
|
||||
출근하기
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleCheckOut}
|
||||
disabled={!canCheckOut}
|
||||
className={`flex-1 h-12 text-base font-medium rounded-lg transition-all ${
|
||||
canCheckOut
|
||||
? 'bg-gray-800 hover:bg-gray-900 text-white'
|
||||
: 'bg-gray-200 text-gray-400 cursor-not-allowed'
|
||||
}`}
|
||||
>
|
||||
퇴근하기
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 범위 밖 경고 */}
|
||||
{!isInRange && distance !== null && (
|
||||
<p className="text-center text-sm text-orange-500">
|
||||
출퇴근 가능 범위({SITE_LOCATION.radius}m) 밖에 있습니다.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
'use client';
|
||||
|
||||
import { useRouter, useParams } from 'next/navigation';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { CardForm } from '@/components/hr/CardManagement/CardForm';
|
||||
import type { Card, CardFormData } from '@/components/hr/CardManagement/types';
|
||||
|
||||
// TODO: 실제 API에서 데이터 가져오기
|
||||
const mockCard: Card = {
|
||||
id: '1',
|
||||
cardCompany: 'shinhan',
|
||||
cardNumber: '1234-1234-1234-1234',
|
||||
cardName: '법인카드1',
|
||||
expiryDate: '0327',
|
||||
pinPrefix: '12',
|
||||
status: 'active',
|
||||
user: {
|
||||
id: 'u1',
|
||||
departmentId: 'd1',
|
||||
departmentName: '부서명',
|
||||
employeeId: 'e1',
|
||||
employeeName: '홍길동',
|
||||
positionId: 'p1',
|
||||
positionName: '팀장',
|
||||
},
|
||||
createdAt: '2024-01-01T00:00:00Z',
|
||||
updatedAt: '2024-01-01T00:00:00Z',
|
||||
};
|
||||
|
||||
export default function CardEditPage() {
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
const [card, setCard] = useState<Card | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
// TODO: API 연동
|
||||
// const id = params.id;
|
||||
setCard(mockCard);
|
||||
}, [params.id]);
|
||||
|
||||
const handleSubmit = (data: CardFormData) => {
|
||||
// TODO: API 연동
|
||||
console.log('Update card:', params.id, data);
|
||||
router.push(`/ko/hr/card-management/${params.id}`);
|
||||
};
|
||||
|
||||
if (!card) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="text-center">
|
||||
<div className="inline-block h-8 w-8 animate-spin rounded-full border-4 border-solid border-primary border-r-transparent mb-4"></div>
|
||||
<p className="text-muted-foreground">로딩 중...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<CardForm
|
||||
mode="edit"
|
||||
card={card}
|
||||
onSubmit={handleSubmit}
|
||||
/>
|
||||
);
|
||||
}
|
||||
110
src/app/[locale]/(protected)/hr/card-management/[id]/page.tsx
Normal file
110
src/app/[locale]/(protected)/hr/card-management/[id]/page.tsx
Normal file
@@ -0,0 +1,110 @@
|
||||
'use client';
|
||||
|
||||
import { useRouter, useParams } from 'next/navigation';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { CardDetail } from '@/components/hr/CardManagement/CardDetail';
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog';
|
||||
import type { Card } from '@/components/hr/CardManagement/types';
|
||||
|
||||
// TODO: 실제 API에서 데이터 가져오기
|
||||
const mockCard: Card = {
|
||||
id: '1',
|
||||
cardCompany: 'shinhan',
|
||||
cardNumber: '1234-1234-1234-1234',
|
||||
cardName: '법인카드1',
|
||||
expiryDate: '0327',
|
||||
pinPrefix: '12',
|
||||
status: 'active',
|
||||
user: {
|
||||
id: 'u1',
|
||||
departmentId: 'd1',
|
||||
departmentName: '부서명',
|
||||
employeeId: 'e1',
|
||||
employeeName: '홍길동',
|
||||
positionId: 'p1',
|
||||
positionName: '팀장',
|
||||
},
|
||||
createdAt: '2024-01-01T00:00:00Z',
|
||||
updatedAt: '2024-01-01T00:00:00Z',
|
||||
};
|
||||
|
||||
export default function CardDetailPage() {
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
const [card, setCard] = useState<Card | null>(null);
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// TODO: API 연동
|
||||
// const id = params.id;
|
||||
setCard(mockCard);
|
||||
}, [params.id]);
|
||||
|
||||
const handleEdit = () => {
|
||||
router.push(`/ko/hr/card-management/${params.id}/edit`);
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
setDeleteDialogOpen(true);
|
||||
};
|
||||
|
||||
const confirmDelete = () => {
|
||||
// TODO: API 연동
|
||||
console.log('Delete card:', params.id);
|
||||
router.push('/ko/hr/card-management');
|
||||
};
|
||||
|
||||
if (!card) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="text-center">
|
||||
<div className="inline-block h-8 w-8 animate-spin rounded-full border-4 border-solid border-primary border-r-transparent mb-4"></div>
|
||||
<p className="text-muted-foreground">로딩 중...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<CardDetail
|
||||
card={card}
|
||||
onEdit={handleEdit}
|
||||
onDelete={handleDelete}
|
||||
/>
|
||||
|
||||
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>카드 삭제</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
"{card.cardName}" 카드를 삭제하시겠습니까?
|
||||
<br />
|
||||
<span className="text-destructive">
|
||||
삭제된 카드 정보는 복구할 수 없습니다.
|
||||
</span>
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>취소</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={confirmDelete}
|
||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||
>
|
||||
삭제
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
22
src/app/[locale]/(protected)/hr/card-management/new/page.tsx
Normal file
22
src/app/[locale]/(protected)/hr/card-management/new/page.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
'use client';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { CardForm } from '@/components/hr/CardManagement/CardForm';
|
||||
import type { CardFormData } from '@/components/hr/CardManagement/types';
|
||||
|
||||
export default function CardNewPage() {
|
||||
const router = useRouter();
|
||||
|
||||
const handleSubmit = (data: CardFormData) => {
|
||||
// TODO: API 연동
|
||||
console.log('Create card:', data);
|
||||
router.push('/ko/hr/card-management');
|
||||
};
|
||||
|
||||
return (
|
||||
<CardForm
|
||||
mode="create"
|
||||
onSubmit={handleSubmit}
|
||||
/>
|
||||
);
|
||||
}
|
||||
7
src/app/[locale]/(protected)/hr/card-management/page.tsx
Normal file
7
src/app/[locale]/(protected)/hr/card-management/page.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { CardManagement } from '@/components/hr/CardManagement';
|
||||
|
||||
export default function CardManagementPage() {
|
||||
return <CardManagement />;
|
||||
}
|
||||
@@ -31,6 +31,10 @@ const mockEmployee: Employee = {
|
||||
departmentPositions: [
|
||||
{ id: '1', departmentId: 'd1', departmentName: '개발본부', positionId: 'p1', positionName: '팀장' }
|
||||
],
|
||||
clockInLocation: 'headquarters',
|
||||
clockOutLocation: 'headquarters',
|
||||
concurrentPosition: '',
|
||||
concurrentReason: '',
|
||||
userInfo: { userId: 'kimcs', role: 'manager', accountStatus: 'active' },
|
||||
createdAt: '2020-03-15T00:00:00Z',
|
||||
updatedAt: '2024-01-15T00:00:00Z',
|
||||
|
||||
@@ -41,6 +41,10 @@ const mockEmployee: Employee = {
|
||||
departmentPositions: [
|
||||
{ id: '1', departmentId: 'd1', departmentName: '개발본부', positionId: 'p1', positionName: '팀장' }
|
||||
],
|
||||
clockInLocation: 'headquarters',
|
||||
clockOutLocation: 'headquarters',
|
||||
concurrentPosition: '',
|
||||
concurrentReason: '',
|
||||
userInfo: { userId: 'kimcs', role: 'manager', accountStatus: 'active' },
|
||||
createdAt: '2020-03-15T00:00:00Z',
|
||||
updatedAt: '2024-01-15T00:00:00Z',
|
||||
|
||||
@@ -430,7 +430,7 @@ export default function EditItemPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="py-6">
|
||||
<div className="p-6">
|
||||
<DynamicItemForm
|
||||
mode="edit"
|
||||
itemType={itemType}
|
||||
|
||||
@@ -284,7 +284,7 @@ export default function ItemDetailPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="py-6">
|
||||
<div className="p-6">
|
||||
<ItemDetailClient item={item} />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -86,7 +86,7 @@ export default function CreateItemPage() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="py-6">
|
||||
<div className="p-6">
|
||||
{submitError && (
|
||||
<div className="mb-4 p-4 bg-red-50 border border-red-200 rounded-lg text-red-900">
|
||||
⚠️ {submitError}
|
||||
|
||||
@@ -12,11 +12,7 @@ import ItemListClient from '@/components/items/ItemListClient';
|
||||
* 품목 목록 페이지
|
||||
*/
|
||||
export default function ItemsPage() {
|
||||
return (
|
||||
<div className="p-6">
|
||||
<ItemListClient />
|
||||
</div>
|
||||
);
|
||||
return <ItemListClient />;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
5
src/app/[locale]/(protected)/payment-history/page.tsx
Normal file
5
src/app/[locale]/(protected)/payment-history/page.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { PaymentHistoryManagement } from '@/components/settings/PaymentHistoryManagement';
|
||||
|
||||
export default function PaymentHistoryPage() {
|
||||
return <PaymentHistoryManagement />;
|
||||
}
|
||||
@@ -190,7 +190,7 @@ export default function EditItemPage() {
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="py-6">
|
||||
<div className="p-6">
|
||||
<div className="text-center py-8">로딩 중...</div>
|
||||
</div>
|
||||
);
|
||||
@@ -201,7 +201,7 @@ export default function EditItemPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="py-6">
|
||||
<div className="p-6">
|
||||
<ItemForm mode="edit" initialData={item} onSubmit={handleSubmit} />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -163,7 +163,7 @@ export default async function ItemDetailPage({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="py-6">
|
||||
<div className="p-6">
|
||||
<Suspense fallback={<div className="text-center py-8">로딩 중...</div>}>
|
||||
<ItemDetailClient item={item} />
|
||||
</Suspense>
|
||||
|
||||
@@ -21,7 +21,7 @@ export default function CreateItemPage() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="py-6">
|
||||
<div className="p-6">
|
||||
<ItemForm mode="create" onSubmit={handleSubmit} />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -145,11 +145,9 @@ export default async function ItemsPage() {
|
||||
const items = await getItems();
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<Suspense fallback={<div className="text-center py-8">로딩 중...</div>}>
|
||||
<ItemListClient />
|
||||
</Suspense>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import ComprehensiveAnalysis from '@/components/reports/ComprehensiveAnalysis';
|
||||
|
||||
export default function ComprehensiveAnalysisPage() {
|
||||
return <ComprehensiveAnalysis />;
|
||||
}
|
||||
6
src/app/[locale]/(protected)/reports/page.tsx
Normal file
6
src/app/[locale]/(protected)/reports/page.tsx
Normal file
@@ -0,0 +1,6 @@
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
export default function ReportsPage() {
|
||||
// 보고서 및 분석 메인 → 종합 경영 분석으로 리다이렉트
|
||||
redirect('/ko/reports/comprehensive-analysis');
|
||||
}
|
||||
@@ -140,6 +140,12 @@ function mapStatus(apiStatus: string, isFinal: boolean): PricingStatus | 'not_re
|
||||
}
|
||||
}
|
||||
|
||||
// 품목 유형 → API item_type_code 매핑 (백엔드 pricing API용)
|
||||
function mapItemTypeCode(itemType?: string): 'PRODUCT' | 'MATERIAL' {
|
||||
// FG(제품)만 PRODUCT, 나머지는 모두 MATERIAL
|
||||
return itemType === 'FG' ? 'PRODUCT' : 'MATERIAL';
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// API 호출 함수
|
||||
// ============================================
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import { AccountInfoClient } from '@/components/settings/AccountInfoManagement';
|
||||
|
||||
export default function AccountInfoPage() {
|
||||
return <AccountInfoClient />;
|
||||
}
|
||||
129
src/app/[locale]/(protected)/settings/accounts/[id]/page.tsx
Normal file
129
src/app/[locale]/(protected)/settings/accounts/[id]/page.tsx
Normal file
@@ -0,0 +1,129 @@
|
||||
'use client';
|
||||
|
||||
import { useParams } from 'next/navigation';
|
||||
import { AccountDetail } from '@/components/settings/AccountManagement/AccountDetail';
|
||||
import type { Account } from '@/components/settings/AccountManagement/types';
|
||||
|
||||
// Mock 데이터 (API 연동 전 임시)
|
||||
const mockAccounts: Account[] = [
|
||||
{
|
||||
id: 'account-1',
|
||||
bankCode: 'shinhan',
|
||||
accountNumber: '1234-1234-1234-1234',
|
||||
accountName: '운영계좌 1',
|
||||
accountHolder: '예금주1',
|
||||
status: 'active',
|
||||
createdAt: '2025-12-19T00:00:00.000Z',
|
||||
updatedAt: '2025-12-19T00:00:00.000Z',
|
||||
},
|
||||
{
|
||||
id: 'account-2',
|
||||
bankCode: 'kb',
|
||||
accountNumber: '1234-1234-1234-1235',
|
||||
accountName: '운영계좌 2',
|
||||
accountHolder: '예금주2',
|
||||
status: 'inactive',
|
||||
createdAt: '2025-12-19T00:00:00.000Z',
|
||||
updatedAt: '2025-12-19T00:00:00.000Z',
|
||||
},
|
||||
{
|
||||
id: 'account-3',
|
||||
bankCode: 'woori',
|
||||
accountNumber: '1234-1234-1234-1236',
|
||||
accountName: '운영계좌 3',
|
||||
accountHolder: '예금주3',
|
||||
status: 'active',
|
||||
createdAt: '2025-12-19T00:00:00.000Z',
|
||||
updatedAt: '2025-12-19T00:00:00.000Z',
|
||||
},
|
||||
{
|
||||
id: 'account-4',
|
||||
bankCode: 'hana',
|
||||
accountNumber: '1234-1234-1234-1237',
|
||||
accountName: '운영계좌 4',
|
||||
accountHolder: '예금주4',
|
||||
status: 'inactive',
|
||||
createdAt: '2025-12-19T00:00:00.000Z',
|
||||
updatedAt: '2025-12-19T00:00:00.000Z',
|
||||
},
|
||||
{
|
||||
id: 'account-5',
|
||||
bankCode: 'nh',
|
||||
accountNumber: '1234-1234-1234-1238',
|
||||
accountName: '운영계좌 5',
|
||||
accountHolder: '예금주5',
|
||||
status: 'active',
|
||||
createdAt: '2025-12-19T00:00:00.000Z',
|
||||
updatedAt: '2025-12-19T00:00:00.000Z',
|
||||
},
|
||||
{
|
||||
id: 'account-6',
|
||||
bankCode: 'ibk',
|
||||
accountNumber: '1234-1234-1234-1239',
|
||||
accountName: '운영계좌 6',
|
||||
accountHolder: '예금주6',
|
||||
status: 'inactive',
|
||||
createdAt: '2025-12-19T00:00:00.000Z',
|
||||
updatedAt: '2025-12-19T00:00:00.000Z',
|
||||
},
|
||||
{
|
||||
id: 'account-7',
|
||||
bankCode: 'shinhan',
|
||||
accountNumber: '1234-1234-1234-1240',
|
||||
accountName: '운영계좌 7',
|
||||
accountHolder: '예금주7',
|
||||
status: 'active',
|
||||
createdAt: '2025-12-19T00:00:00.000Z',
|
||||
updatedAt: '2025-12-19T00:00:00.000Z',
|
||||
},
|
||||
{
|
||||
id: 'account-8',
|
||||
bankCode: 'kb',
|
||||
accountNumber: '1234-1234-1234-1241',
|
||||
accountName: '운영계좌 8',
|
||||
accountHolder: '예금주8',
|
||||
status: 'inactive',
|
||||
createdAt: '2025-12-19T00:00:00.000Z',
|
||||
updatedAt: '2025-12-19T00:00:00.000Z',
|
||||
},
|
||||
{
|
||||
id: 'account-9',
|
||||
bankCode: 'woori',
|
||||
accountNumber: '1234-1234-1234-1242',
|
||||
accountName: '운영계좌 9',
|
||||
accountHolder: '예금주9',
|
||||
status: 'active',
|
||||
createdAt: '2025-12-19T00:00:00.000Z',
|
||||
updatedAt: '2025-12-19T00:00:00.000Z',
|
||||
},
|
||||
{
|
||||
id: 'account-10',
|
||||
bankCode: 'hana',
|
||||
accountNumber: '1234-1234-1234-1243',
|
||||
accountName: '운영계좌 10',
|
||||
accountHolder: '예금주10',
|
||||
status: 'inactive',
|
||||
createdAt: '2025-12-19T00:00:00.000Z',
|
||||
updatedAt: '2025-12-19T00:00:00.000Z',
|
||||
},
|
||||
];
|
||||
|
||||
export default function AccountDetailPage() {
|
||||
const params = useParams();
|
||||
const accountId = params.id as string;
|
||||
|
||||
// Mock: 계좌 조회
|
||||
const account = mockAccounts.find(a => a.id === accountId);
|
||||
|
||||
if (!account) {
|
||||
return (
|
||||
<div className="p-6">
|
||||
<div className="text-center py-8 text-muted-foreground">
|
||||
계좌를 찾을 수 없습니다.
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <AccountDetail account={account} mode="view" />;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { AccountDetail } from '@/components/settings/AccountManagement/AccountDetail';
|
||||
|
||||
export default function NewAccountPage() {
|
||||
return <AccountDetail mode="create" />;
|
||||
}
|
||||
7
src/app/[locale]/(protected)/settings/accounts/page.tsx
Normal file
7
src/app/[locale]/(protected)/settings/accounts/page.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { AccountManagement } from '@/components/settings/AccountManagement';
|
||||
|
||||
export default function AccountsPage() {
|
||||
return <AccountManagement />;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { AttendanceSettingsManagement } from '@/components/settings/AttendanceSettingsManagement';
|
||||
|
||||
export default function AttendanceSettingsPage() {
|
||||
return <AttendanceSettingsManagement />;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { NotificationSettingsManagement } from '@/components/settings/NotificationSettings';
|
||||
|
||||
export default function NotificationSettingsPage() {
|
||||
return <NotificationSettingsManagement />;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
'use client';
|
||||
|
||||
import { useParams } from 'next/navigation';
|
||||
import { PopupForm } from '@/components/settings/PopupManagement';
|
||||
import { MOCK_POPUPS } from '@/components/settings/PopupManagement/types';
|
||||
|
||||
export default function PopupEditPage() {
|
||||
const params = useParams();
|
||||
const id = params.id as string;
|
||||
|
||||
// Mock: ID로 팝업 데이터 조회
|
||||
const popup = MOCK_POPUPS.find((p) => p.id === id);
|
||||
|
||||
if (!popup) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<p className="text-muted-foreground">팝업을 찾을 수 없습니다.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <PopupForm mode="edit" initialData={popup} />;
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
'use client';
|
||||
|
||||
import { useRouter, useParams } from 'next/navigation';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { PopupDetail } from '@/components/settings/PopupManagement';
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog';
|
||||
import { MOCK_POPUPS, type Popup } from '@/components/settings/PopupManagement/types';
|
||||
|
||||
export default function PopupDetailPage() {
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
const [popup, setPopup] = useState<Popup | null>(null);
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// TODO: API 연동
|
||||
const id = params.id as string;
|
||||
const found = MOCK_POPUPS.find((p) => p.id === id);
|
||||
setPopup(found || null);
|
||||
}, [params.id]);
|
||||
|
||||
const handleEdit = () => {
|
||||
router.push(`/ko/settings/popup-management/${params.id}/edit`);
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
setDeleteDialogOpen(true);
|
||||
};
|
||||
|
||||
const confirmDelete = () => {
|
||||
// TODO: API 연동
|
||||
console.log('Delete popup:', params.id);
|
||||
router.push('/ko/settings/popup-management');
|
||||
};
|
||||
|
||||
if (!popup) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="text-center">
|
||||
<div className="inline-block h-8 w-8 animate-spin rounded-full border-4 border-solid border-primary border-r-transparent mb-4"></div>
|
||||
<p className="text-muted-foreground">로딩 중...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<PopupDetail
|
||||
popup={popup}
|
||||
onEdit={handleEdit}
|
||||
onDelete={handleDelete}
|
||||
/>
|
||||
|
||||
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>팝업 삭제</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
"{popup.title}" 팝업을 삭제하시겠습니까?
|
||||
<br />
|
||||
<span className="text-destructive">
|
||||
삭제된 팝업 정보는 복구할 수 없습니다.
|
||||
</span>
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>취소</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={confirmDelete}
|
||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||
>
|
||||
삭제
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user