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:
byeongcheolryu
2025-12-19 19:12:34 +09:00
parent d742c0ce26
commit c6b605200d
213 changed files with 32644 additions and 775 deletions

View File

@@ -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 (하위 페이지 정리, 리스트 페이지만 유지)

View File

@@ -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/ - 레거시/완료된 문서
완료되거나 더 이상 활성화되지 않은 문서들. 참조용으로 보관.

View 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 패턴

View File

@@ -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

View 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
```

View File

@@ -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 영역 (수취 어음 등록 시 표시, 메모 입력박스)은 현재 스코프에서 제외
- 기본 기능 먼저 구현 후 추가 기능 논의

View 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]`

View File

@@ -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)

View File

@@ -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` |

View File

@@ -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

View File

@@ -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`

View 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
**상태**: 계획 검토 대기

View File

@@ -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 | 마무리 | - |
---
**확인 후 작업 시작하겠습니다!**

View File

@@ -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*

View File

@@ -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`

View File

@@ -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 관련 유사 이슈

View File

@@ -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)
- 컴포넌트 내부: 추가 패딩 선택적
```

View 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 테스트 후 진행

View 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

View File

@@ -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
---
## 리스크 및 롤백 계획
### 리스크 평가

View 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 수신 동의 | 체크박스 | 동의철회일시 표시 |

View File

@@ -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에 전달하여 계좌 내역 자동 수신
- 연동 성공 시 해당 계좌의 사용 내역이 자동으로 시스템에 반영됨
- 해당 타인에는 은행에서 빠른 조회 서비스 사전 등록 필요
### 삭제 시 주의사항
- 삭제된 계좌의 과거 사용 내역은 보존

View 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 (로고, 사업자등록증)

View 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 업데이트 | ✅ |

View File

@@ -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
View File

@@ -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",

View File

@@ -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",

View File

@@ -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} />;
}

View File

@@ -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} />;
}

View File

@@ -0,0 +1,7 @@
'use client';
import { BadDebtDetail } from '@/components/accounting/BadDebtCollection/BadDebtDetail';
export default function NewBadDebtPage() {
return <BadDebtDetail mode="new" />;
}

View File

@@ -0,0 +1,7 @@
'use client';
import { BadDebtCollection } from '@/components/accounting/BadDebtCollection';
export default function BadDebtCollectionPage() {
return <BadDebtCollection />;
}

View File

@@ -0,0 +1,7 @@
'use client';
import { BankTransactionInquiry } from '@/components/accounting/BankTransactionInquiry';
export default function BankTransactionsPage() {
return <BankTransactionInquiry />;
}

View 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} />;
}

View File

@@ -0,0 +1,7 @@
'use client';
import { BillDetail } from '@/components/accounting/BillManagement/BillDetail';
export default function BillNewPage() {
return <BillDetail billId="new" mode="new" />;
}

View 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} />;
}

View File

@@ -0,0 +1,5 @@
import { CardTransactionInquiry } from '@/components/accounting/CardTransactionInquiry';
export default function CardTransactionsPage() {
return <CardTransactionInquiry />;
}

View File

@@ -0,0 +1,7 @@
'use client';
import { DailyReport } from '@/components/accounting/DailyReport';
export default function DailyReportPage() {
return <DailyReport />;
}

View File

@@ -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} />;
}

View File

@@ -0,0 +1,5 @@
import { DepositManagement } from '@/components/accounting/DepositManagement';
export default function DepositsPage() {
return <DepositManagement />;
}

View File

@@ -0,0 +1,5 @@
import { ExpectedExpenseManagement } from '@/components/accounting/ExpectedExpenseManagement';
export default function ExpectedExpensesPage() {
return <ExpectedExpenseManagement />;
}

View File

@@ -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} />;
}

View File

@@ -0,0 +1,5 @@
import { PurchaseManagement } from '@/components/accounting/PurchaseManagement';
export default function PurchasePage() {
return <PurchaseManagement />;
}

View File

@@ -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} />;
}

View 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} />;
}

View File

@@ -0,0 +1,5 @@
import { SalesDetail } from '@/components/accounting/SalesManagement/SalesDetail';
export default function NewSalesPage() {
return <SalesDetail mode="new" />;
}

View File

@@ -0,0 +1,5 @@
import { SalesManagement } from '@/components/accounting/SalesManagement';
export default function SalesPage() {
return <SalesManagement />;
}

View File

@@ -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} />;
}

View File

@@ -0,0 +1,7 @@
'use client';
import { VendorLedger } from '@/components/accounting/VendorLedger';
export default function VendorLedgerPage() {
return <VendorLedger />;
}

View 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} />;
}

View File

@@ -0,0 +1,7 @@
'use client';
import { VendorDetail } from '@/components/accounting/VendorManagement/VendorDetail';
export default function NewVendorPage() {
return <VendorDetail mode="new" />;
}

View File

@@ -0,0 +1,7 @@
'use client';
import { VendorManagement } from '@/components/accounting/VendorManagement';
export default function VendorsPage() {
return <VendorManagement />;
}

View File

@@ -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} />;
}

View File

@@ -0,0 +1,5 @@
import { WithdrawalManagement } from '@/components/accounting/WithdrawalManagement';
export default function WithdrawalsPage() {
return <WithdrawalManagement />;
}

View 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} />;
}

View 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}
/>
);
}

View File

@@ -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}
/>
);
}

View File

@@ -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>
&quot;{board.boardName}&quot; ?
<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>
</>
);
}

View File

@@ -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}
/>
);
}

View File

@@ -0,0 +1,7 @@
'use client';
import { BoardManagement } from '@/components/board/BoardManagement';
export default function BoardManagementPage() {
return <BoardManagement />;
}

View File

@@ -0,0 +1,14 @@
/**
* 게시글 등록 페이지
*/
import { BoardForm } from '@/components/board/BoardForm';
export default function BoardCreatePage() {
return <BoardForm mode="create" />;
}
export const metadata = {
title: '게시글 등록',
description: '게시글을 등록하고 관리합니다.',
};

View File

@@ -0,0 +1,14 @@
/**
* 게시판 목록 페이지
*/
import { BoardList } from '@/components/board/BoardList';
export default function BoardPage() {
return <BoardList />;
}
export const metadata = {
title: '게시판',
description: '게시판의 게시글을 등록하고 관리합니다.',
};

View File

@@ -0,0 +1,5 @@
import { CompanyInfoManagement } from '@/components/settings/CompanyInfoManagement';
export default function CompanyInfoPage() {
return <CompanyInfoManagement />;
}

View File

@@ -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} />;
}

View File

@@ -0,0 +1,7 @@
'use client';
import { EventList } from '@/components/customer-center/EventManagement';
export default function EventsPage() {
return <EventList />;
}

View File

@@ -0,0 +1,7 @@
'use client';
import { FAQList } from '@/components/customer-center/FAQManagement';
export default function FAQPage() {
return <FAQList />;
}

View File

@@ -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} />;
}

View File

@@ -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}
/>
);
}

View File

@@ -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 문의를 등록합니다.',
};

View File

@@ -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 문의를 등록하고 답변을 확인합니다.',
};

View File

@@ -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} />;
}

View File

@@ -0,0 +1,7 @@
'use client';
import { NoticeList } from '@/components/customer-center/NoticeManagement';
export default function NoticesPage() {
return <NoticeList />;
}

View 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>
);
}

View 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;

View 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>
);
}

View File

@@ -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}
/>
);
}

View 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>
&quot;{card.cardName}&quot; ?
<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>
</>
);
}

View 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}
/>
);
}

View File

@@ -0,0 +1,7 @@
'use client';
import { CardManagement } from '@/components/hr/CardManagement';
export default function CardManagementPage() {
return <CardManagement />;
}

View File

@@ -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',

View File

@@ -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',

View File

@@ -430,7 +430,7 @@ export default function EditItemPage() {
}
return (
<div className="py-6">
<div className="p-6">
<DynamicItemForm
mode="edit"
itemType={itemType}

View File

@@ -284,7 +284,7 @@ export default function ItemDetailPage() {
}
return (
<div className="py-6">
<div className="p-6">
<ItemDetailClient item={item} />
</div>
);

View File

@@ -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}

View File

@@ -12,11 +12,7 @@ import ItemListClient from '@/components/items/ItemListClient';
* 품목 목록 페이지
*/
export default function ItemsPage() {
return (
<div className="p-6">
<ItemListClient />
</div>
);
return <ItemListClient />;
}
/**

View File

@@ -0,0 +1,5 @@
import { PaymentHistoryManagement } from '@/components/settings/PaymentHistoryManagement';
export default function PaymentHistoryPage() {
return <PaymentHistoryManagement />;
}

View File

@@ -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>
);

View File

@@ -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>

View File

@@ -21,7 +21,7 @@ export default function CreateItemPage() {
};
return (
<div className="py-6">
<div className="p-6">
<ItemForm mode="create" onSubmit={handleSubmit} />
</div>
);

View File

@@ -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>
);
}

View File

@@ -0,0 +1,5 @@
import ComprehensiveAnalysis from '@/components/reports/ComprehensiveAnalysis';
export default function ComprehensiveAnalysisPage() {
return <ComprehensiveAnalysis />;
}

View File

@@ -0,0 +1,6 @@
import { redirect } from 'next/navigation';
export default function ReportsPage() {
// 보고서 및 분석 메인 → 종합 경영 분석으로 리다이렉트
redirect('/ko/reports/comprehensive-analysis');
}

View File

@@ -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 호출 함수
// ============================================

View File

@@ -0,0 +1,5 @@
import { AccountInfoClient } from '@/components/settings/AccountInfoManagement';
export default function AccountInfoPage() {
return <AccountInfoClient />;
}

View 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" />;
}

View File

@@ -0,0 +1,7 @@
'use client';
import { AccountDetail } from '@/components/settings/AccountManagement/AccountDetail';
export default function NewAccountPage() {
return <AccountDetail mode="create" />;
}

View File

@@ -0,0 +1,7 @@
'use client';
import { AccountManagement } from '@/components/settings/AccountManagement';
export default function AccountsPage() {
return <AccountManagement />;
}

View File

@@ -0,0 +1,5 @@
import { AttendanceSettingsManagement } from '@/components/settings/AttendanceSettingsManagement';
export default function AttendanceSettingsPage() {
return <AttendanceSettingsManagement />;
}

View File

@@ -0,0 +1,5 @@
import { NotificationSettingsManagement } from '@/components/settings/NotificationSettings';
export default function NotificationSettingsPage() {
return <NotificationSettingsManagement />;
}

View File

@@ -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} />;
}

View File

@@ -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>
&quot;{popup.title}&quot; ?
<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