feat: 공지 팝업 시스템 구현 및 캘린더/어음/팝업관리 개선

- NoticePopupModal: 공지 팝업 컨테이너/actions 신규 구현
- AuthenticatedLayout에 공지 팝업 연동
- CalendarSection: 일정 타입 확장 및 UI 개선
- BillManagementClient: 기능 확장
- PopupManagement: popupDetailConfig 대폭 확장, 상세/폼 개선
- BoardForm/BoardManagement: 게시판 폼 개선
- LoginPage, logout, userStorage: 인증 관련 소폭 수정
- dashboard types 정비
- claudedocs: 공지팝업 구현, 캘린더 어음연동/일정타입, API changelog 문서 추가
This commit is contained in:
유병철
2026-03-10 15:16:41 +09:00
parent 7bd4bd38da
commit 397eb2c19c
23 changed files with 1004 additions and 79 deletions

View File

@@ -0,0 +1,103 @@
# [IMPL] 공지 팝업 사용자 표시 연동
> 관리자가 등록한 팝업을 사용자에게 자동 표시하는 기능 구현
## 현황
| 구분 | 상태 |
|------|------|
| 관리자 팝업 관리 UI (CRUD) | ✅ 완성 |
| 백엔드 API (`/api/v1/popups`) | ✅ 완성 |
| `NoticePopupModal` 표시 컴포넌트 | ✅ 완성 |
| 활성 팝업 조회 서버 액션 | ✅ 완성 |
| 레이아웃 자동 표시 연동 | ✅ 완성 |
| 부서별 팝업 필터링 (백엔드) | ✅ 완성 (2026-03-10) |
| 부서별 팝업 필터링 (프론트) | ✅ 완성 (2026-03-10) |
| 부서 선택 UI (관리자 폼) | ✅ 완성 (2026-03-10) |
## 구현 범위 (프론트만)
### 1. `getActivePopups()` 서버 액션
- 위치: `src/components/common/NoticePopupModal/actions.ts`
- `GET /api/v1/popups?status=active` 호출
- 기존 `PopupApiData``NoticePopupData` 변환
### 2. `NoticePopupContainer` 컴포넌트
- 위치: `src/components/common/NoticePopupModal/NoticePopupContainer.tsx`
- 로그인 후 활성 팝업 fetch
- `isPopupDismissedForToday()` 필터링
- 여러 개 팝업 순차 표시 (하나 닫으면 다음 팝업)
### 3. `AuthenticatedLayout` 연동
- `NoticePopupContainer` 렌더링 추가
## 기존 파일 활용
```
src/components/common/NoticePopupModal/
├── NoticePopupModal.tsx ← 기존 (수정 없음)
├── NoticePopupContainer.tsx ← 신규
└── actions.ts ← 신규
src/components/settings/PopupManagement/
├── utils.ts ← transformApiToFrontend 재사용
└── types.ts ← PopupApiData 타입 재사용
src/layouts/AuthenticatedLayout.tsx ← NoticePopupContainer 추가
```
## 동작 흐름
```
로그인 → AuthenticatedLayout 마운트
→ NoticePopupContainer useEffect
→ localStorage에서 user.department_id 조회
→ getActivePopups(departmentId) API 호출
→ 백엔드 scopeForUser(departmentId) 적용
→ target_type='all' 팝업 + 해당 부서 팝업 반환
→ 날짜 범위(startDate~endDate) 필터
→ isPopupDismissedForToday() 필터
→ 표시할 팝업 있으면 첫 번째 팝업 모달 표시
→ 닫기 클릭 → "오늘 하루 안 보기" 체크 시 localStorage 저장
→ 다음 팝업 표시 (없으면 종료)
```
---
## [2026-03-10] 부서별 팝업 필터링 + 부서 선택 UI
### 배경
팝업 대상이 "부서별"일 때 어떤 부서인지 선택할 수 없었고, 사용자에게도 부서 기반 필터링이 적용되지 않았음.
### 변경사항
#### 백엔드 (sam-api)
- `MemberService::getUserInfoForLogin()` — 로그인 응답에 `department_id` 추가
- `PopupService``scopeForUser(?int $departmentId)` 스코프로 부서별 필터링
#### 프론트엔드
| 파일 | 변경 |
|------|------|
| `LoginPage.tsx` | localStorage user에 `department_id` 저장 |
| `NoticePopupContainer.tsx` | `user.department_id``getActivePopups()`에 전달 |
| `popupDetailConfig.ts` | `target` 필드를 custom 렌더로 변경, `TargetSelectorField` 컴포넌트 추가 |
| `PopupDetailClientV2.tsx` | `handleSubmit`에서 `decodeTargetValue()``targetDepartmentId` 추출 |
| `types.ts` | `Popup.targetId`, `Popup.targetName` 필드 추가 |
| `utils.ts` | `transformApiToFrontend``targetId`, `targetName` 매핑 추가 |
| `actions.ts` | `getDepartmentList()` 서버 액션 추가 |
### 핵심 구현: 대상 필드 값 인코딩
```typescript
// 단일 form field에 target_type + department_id를 함께 저장
encodeTargetValue('department', 13) 'department:13'
decodeTargetValue('department:13') { targetType: 'department', departmentId: 13 }
encodeTargetValue('all') 'all'
```
### TargetSelectorField 동작
```
대상 Select: [전사 | 부서별]
→ "부서별" 선택 시 → getDepartmentList() API 호출
→ 부서 Select 추가 표시: [개발팀 | 영업팀 | ...]
→ 부서 선택 시 form value = 'department:13'
```

View File

@@ -0,0 +1,45 @@
# 어음 만기일 캘린더 연동
**날짜**: 2026-03-10
**범위**: Backend (CalendarService) + Frontend (CalendarSection)
## 변경 요약
대시보드 캘린더에 어음(Bill) 만기일을 5번째 데이터 소스로 추가.
기존 4개 소스(작업지시, 계약, 휴가, 범용일정)와 동일한 패턴.
## Backend 변경
### `app/Services/CalendarService.php`
- `use App\Models\Tenants\Bill` import 추가
- `getSchedules()`: `$type === 'bill'` 필터 조건 및 merge 추가
- `getBillSchedules()` 메서드 신규:
- `maturity_date` 기준 날짜 범위 필터
- `paymentComplete`, `dishonored` 상태 제외
- 아이템 형식: `bill_{id}`, `[만기] {거래처명} {금액}원`
- `type: 'bill'`, `isAllDay: true`
## Frontend 변경
### `src/lib/api/dashboard/types.ts`
- `CalendarScheduleType``'bill'` 추가
### `src/components/business/CEODashboard/types.ts`
- `CalendarScheduleItem.type``'bill'` 추가
- `CalendarTaskFilterType``'bill'` 추가
### `src/components/business/CEODashboard/sections/CalendarSection.tsx`
- `SCHEDULE_TYPE_COLORS`: `bill: 'amber'`
- `SCHEDULE_TYPE_LABELS`: `bill: '어음'`
- `SCHEDULE_TYPE_BADGE_COLORS`: `bill: amber 배지 스타일`
- `TASK_FILTER_OPTIONS`: `{ value: 'bill', label: '어음' }`
- `ExtendedTaskFilterType`: `'bill'` 추가
- 모바일 리스트뷰 `colorMap`: `bill: 'bg-amber-500'`
## 검증 방법
1. 대시보드 캘린더에서 어음 만기일이 amber 색상 점으로 표시되는지 확인
2. 캘린더 필터에서 "어음" 선택 시 어음 일정만 필터링되는지 확인
3. 어음 만기일 클릭 시 `[만기] 거래처명 금액원` 형식으로 표시되는지 확인
4. 기존 일정(일정/발주/시공/기타) 정상 동작 확인

View File

@@ -0,0 +1,122 @@
# sam-api 변경 내역 (2026-03-09)
**13개 커밋** (중복 1건 제외 실질 12건)
---
## feat: 신규 기능 (6건)
### 1. [database] codebridge 이관 완료 테이블 58개 삭제
- **커밋**: `28ae481` / `74e3c21` (동일 커밋 2건)
- **작업자**: 권혁성
- **변경 파일**: 마이그레이션 1개
- **내용**:
- sam DB → codebridge DB 이관 완료된 58개 테이블 DROP
- FK 체크 비활성화 후 일괄 삭제
- 복원 경로: `~/backups/sam_codebridge_tables_20260309.sql`
### 2. [결재] 테넌트 부트스트랩에 기본 결재 양식 자동 시딩
- **커밋**: `45a207d`
- **작업자**: 권혁성
- **변경 파일**: `RecipeRegistry.php`, `ApprovalFormsStep.php` (신규)
- **내용**:
- ApprovalFormsStep 신규 생성 (proposal, expenseReport, expenseEstimate, attendance_request, reason_report)
- RecipeRegistry STANDARD 레시피에 등록
- 테넌트 생성 시 자동 실행, 기존 테넌트는 `php artisan tenants:bootstrap --all`
### 3. [quality] 검사 상태 자동 재계산 + 수주처 선택 연동
- **커밋**: `3fc5f51`
- **작업자**: 권혁성
- **변경 파일**: `QualityDocumentLocation.php`, `QualityDocumentService.php`
- **내용**:
- 개소별 inspection_status를 검사 데이터 기반 자동 판정 (15개 판정필드 + 사진 유무 → pending/in_progress/completed)
- 문서 status를 개소 상태 집계로 자동 재계산
- transformToFrontend에 client_id 매핑 추가
### 4. [현황판/악성채권] 카드별 sub_label 추가
- **커밋**: `56c60ec`
- **작업자**: 유병철
- **변경 파일**: `BadDebtService.php`, `StatusBoardService.php`
- **내용**:
- BadDebtService: 카드별(전체/추심중/법적조치/회수완료) sub_labels 추가
- StatusBoardService: 악성채권(최다 금액 거래처명), 신규거래처(최근 등록 업체명), 결재(최근 결재 제목) sub_label 추가
### 5. [복리후생] 상세 조회 커스텀 날짜 범위 필터
- **커밋**: `60c4256`
- **작업자**: 유병철
- **변경 파일**: `WelfareController.php`, `WelfareService.php`
- **내용**:
- start_date, end_date 쿼리 파라미터 추가
- 커스텀 날짜 범위 지정 시 해당 범위로 일별 사용 내역 조회
- 미지정 시 기존 분기 기준 유지
### 6. [finance] 더존 Smart A 표준 계정과목 추가 시딩
- **커밋**: `1d5d161`
- **작업자**: 유병철
- **변경 파일**: 마이그레이션 1개 (467줄)
- **내용**:
- 기획서 14장 기준 누락분 보완
- tenant_id + code 중복 시 skip (기존 데이터 보호)
---
## fix: 버그 수정 (4건)
### 7. [현황판] 결재 카드 조회에 approvalOnly 스코프 추가
- **커밋**: `ee9f4d0`
- **작업자**: 유병철
- **변경 파일**: `StatusBoardService.php`
- **내용**: ApprovalStep 쿼리에 approvalOnly() 스코프 적용, 결재 유형만 필터링
### 8. [악성채권] tenant_id ambiguous 에러 + JOIN 컬럼 prefix 보완
- **커밋**: `3929c5f`, `ca259cc`
- **작업자**: 유병철
- **변경 파일**: `BadDebtService.php`, `StatusBoardService.php`
- **내용**:
- JOIN 쿼리에서 `bad_debts.tenant_id`로 테이블 명시
- is_active, status 컬럼에도 `bad_debts.` prefix 추가
### 9. [세금계산서] NOT NULL 컬럼 null 방어 처리
- **커밋**: `1861f4d`
- **작업자**: 유병철
- **변경 파일**: `TaxInvoiceService.php`
- **내용**: supplier/buyer corp_num, corp_name null→빈문자열 보정 (ConvertEmptyStringsToNull 미들웨어 대응)
### 10. [세금계산서] 매입/매출 방향별 필수값 조건 분리
- **커밋**: `c62e59a`
- **작업자**: 유병철
- **변경 파일**: `CreateTaxInvoiceRequest.php`
- **내용**: 매입(supplier 필수), 매출(buyer 필수) — `required → required_if:direction` 조건부 검증
---
## refactor: 리팩토링 (1건)
### 11. [세금계산서/바로빌] ApiResponse::handle() 클로저 패턴 통일
- **커밋**: `e6f13e3`
- **작업자**: 유병철
- **변경 파일**: `BarobillSettingController.php`, `TaxInvoiceController.php`
- **내용**:
- 전체 액션 클로저 방식 전환 (show/save/testConnection, index/show/store/update/destroy/issue/bulkIssue/cancel/checkStatus/summary)
- 중간 변수 할당 제거, 일관된 응답 패턴 적용
- **-38줄** (91→40+27 구조 정리)
---
## 영향받는 주요 서비스 파일
| 파일 | 변경 횟수 | 도메인 |
|------|----------|--------|
| `StatusBoardService.php` | 4회 | 현황판/대시보드 |
| `BadDebtService.php` | 3회 | 악성채권 |
| `TaxInvoiceService.php` | 1회 | 세금계산서 |
| `TaxInvoiceController.php` | 1회 | 세금계산서 |
| `QualityDocumentService.php` | 1회 | 품질검사 |
| `WelfareService.php` | 1회 | 복리후생 |
## 작업자별 커밋 수
| 작업자 | 커밋 수 | 주요 도메인 |
|--------|---------|-------------|
| 유병철 | 9건 | 현황판, 악성채권, 세금계산서, 복리후생, 계정과목 |
| 권혁성 | 4건 | DB 이관, 결재 시딩, 품질검사 |

View File

@@ -0,0 +1,77 @@
# 캘린더 신규 일정 타입 추가 (결제예정/납기/출고)
**작업일**: 2026-03-10
**목적**: CEO 대시보드 캘린더에서 자금/물류/납기 일정을 한눈에 파악
---
## 추가된 타입
| 타입 | 라벨 | 색상 | ID 형식 | 제목 형식 |
|------|------|------|---------|----------|
| `expected_expense` | 결제예정 | rose (분홍) | `expense_{id}` | `[결제] {거래처명} {금액}원` |
| `delivery` | 납기 | cyan (청록) | `delivery_{id}` | `[납기] {거래처명} {현장명 or 수주번호}` |
| `shipment` | 출고 | teal (틸) | `shipment_{id}` | `[출고] {거래처명} {현장명 or 출하번호}` |
## 제외 항목
| 항목 | 사유 |
|------|------|
| 미수금 입금 예정일 | `Deposit` 모델에 expected_date 필드 없음 → Phase 2 |
| 세금 납부 예정일 | 이미 CalendarScheduleStore + 상수로 orange 색상 표시 중 |
---
## 변경 파일
### Backend (1파일)
**`app/Services/CalendarService.php`**
- import 추가: `Order`, `ExpectedExpense`, `Shipment`
- `getSchedules()`: 3개 merge 블록 추가 (`expected_expense`, `delivery`, `shipment`)
- 신규 private 메서드 3개:
- `getExpectedExpenseSchedules()``ExpectedExpense` 모델, `expected_payment_date`, `payment_status != 'paid'`
- `getDeliverySchedules()``Order` 모델, `delivery_date`, 활성 status_code 5개
- `getShipmentSchedules()``Shipment` 모델, `scheduled_date`, status in ('scheduled', 'ready')
### Frontend (3파일)
**`src/components/business/CEODashboard/types.ts`**
- `CalendarScheduleItem.type` union에 3개 타입 추가
- `CalendarTaskFilterType` union에 3개 타입 추가
**`src/lib/api/dashboard/types.ts`**
- `CalendarScheduleType` union에 3개 타입 추가
**`src/components/business/CEODashboard/sections/CalendarSection.tsx`**
- `SCHEDULE_TYPE_COLORS`: rose/cyan/teal 추가
- `SCHEDULE_TYPE_ROUTES`: 3개 라우트 추가
- `SCHEDULE_TYPE_LABELS`: 결제예정/납기/출고 추가
- `SCHEDULE_TYPE_BADGE_COLORS`: rose/cyan/teal 뱃지 스타일 추가
- `TASK_FILTER_OPTIONS`: 필터 드롭다운 옵션 3개 추가
- `ExtendedTaskFilterType`: `'bill'` 제거 (CalendarTaskFilterType에 이미 포함)
- `getScheduleLink()`: `expected_expense`는 목록 페이지만 이동 (상세 없음)
- 모바일 `colorMap`: 3개 dot 색상 추가
---
## 라우트 매핑
| 타입 | 상세보기 클릭 시 이동 경로 | 비고 |
|------|--------------------------|------|
| `expected_expense` | `/ko/accounting/expected-expenses` | 목록 페이지 (상세 없음) |
| `delivery` | `/ko/sales/order-management-sales/{id}` | 수주 상세 |
| `shipment` | `/ko/outbound/shipments/{id}` | 출고 상세 |
---
## 검수 결과 (2026-03-10)
- [x] 캘린더 '전체' 필터에서 결제예정 항목 표시
- [x] 필터 드롭다운에 결제예정/납기/출고 옵션 추가
- [x] 결제예정 필터 선택 시 해당 타입만 표시
- [x] 결제예정 상세보기 링크 동작
- [x] 결제예정 뱃지 rose 색상 표시
- [x] 기존 5개 타입 정상 동작
- [x] TypeScript 빌드 에러 없음
- [ ] 납기/출고 데이터 표시 (테스트 DB에 해당 날짜 데이터 없어 미확인 — 기능은 정상)

View File

@@ -0,0 +1,166 @@
# [TODO] 유저 개별 설정 DB 이관 계획
> 현재 localStorage에 저장 중인 유저별 설정을 백엔드 DB로 이관하여 크로스 디바이스 동기화 지원
---
## 현재 현황: localStorage 기반 유저 설정 목록
### 🔴 HIGH — 우선 이관 대상
| 항목 | 저장 키 | 파일 | 유저 분리 | 설명 |
|------|---------|------|-----------|------|
| 즐겨찾기 | `sam-favorites-{userId}` | `stores/favoritesStore.ts` | ✅ | 메뉴 즐겨찾기 (최대 10개) |
| 테이블 컬럼 설정 | `sam-table-columns-{userId}` | `stores/useTableColumnStore.ts` | ✅ | 컬럼 너비, 숨김 여부 (페이지별) |
### 🟡 MEDIUM — 2차 이관 대상
| 항목 | 저장 키 | 파일 | 유저 분리 | 설명 |
|------|---------|------|-----------|------|
| 테마 | `theme` | `stores/themeStore.ts` | ❌ 공용 | light / dark / senior |
| 글꼴 크기 | `sam-font-size` | `layouts/AuthenticatedLayout.tsx` | ❌ 공용 | 12~20px (기본 16) |
| 사이드바 접힘 | `sam-menu` | `stores/menuStore.ts` | ❌ 공용 | sidebarCollapsed 상태 |
| 알림 설정 | `ITEM_VISIBILITY_STORAGE_KEY` | `settings/NotificationSettings/index.tsx` | ❌ 공용 | 알림 카테고리별 표시 여부 |
### 🟢 LOW — 선택적 이관
| 항목 | 저장 키 | 파일 | 설명 |
|------|---------|------|------|
| 팝업 오늘 하루 안 보기 | `popup_dismissed_{id}` | `common/NoticePopupModal.tsx` | 매일 자동 리셋, 임시성 |
### ❌ 제외 (이관 불필요)
| 항목 | 이유 |
|------|------|
| Auth 토큰 (HttpOnly 쿠키) | 이미 서버 관리 |
| Auth Store (mes-users, mes-currentUser) | 인증 플로우 전용 |
| Master Data 캐시 (sessionStorage) | TTL 기반 캐시, 설정 아님 |
| Dashboard Stale 캐시 (sessionStorage) | 세션 캐시 |
| Page Builder (page-builder-pages) | 개발 전용 도구 |
---
## 백엔드 DB 스키마 (안)
### user_preferences (통합 설정 테이블)
```sql
CREATE TABLE user_preferences (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
tenant_id BIGINT UNSIGNED NOT NULL,
user_id BIGINT UNSIGNED NOT NULL,
theme VARCHAR(20) DEFAULT 'light',
font_size TINYINT UNSIGNED DEFAULT 16,
sidebar_collapsed BOOLEAN DEFAULT false,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY (tenant_id, user_id)
);
```
### user_favorites (즐겨찾기)
```sql
CREATE TABLE user_favorites (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
tenant_id BIGINT UNSIGNED NOT NULL,
user_id BIGINT UNSIGNED NOT NULL,
menu_id VARCHAR(100) NOT NULL,
label VARCHAR(255) NOT NULL,
icon_name VARCHAR(100),
path VARCHAR(500) NOT NULL,
display_order TINYINT UNSIGNED DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY (tenant_id, user_id, menu_id)
);
```
### user_table_preferences (테이블 컬럼 설정)
```sql
CREATE TABLE user_table_preferences (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
tenant_id BIGINT UNSIGNED NOT NULL,
user_id BIGINT UNSIGNED NOT NULL,
page_id VARCHAR(100) NOT NULL,
settings JSON NOT NULL, -- { columnWidths: {...}, hiddenColumns: [...] }
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY (tenant_id, user_id, page_id)
);
```
### user_notification_preferences (알림 설정)
```sql
CREATE TABLE user_notification_preferences (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
tenant_id BIGINT UNSIGNED NOT NULL,
user_id BIGINT UNSIGNED NOT NULL,
settings JSON NOT NULL,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY (tenant_id, user_id)
);
```
---
## API 엔드포인트 (안)
### Phase 1 (즐겨찾기 + 테이블 설정)
```
GET /api/v1/user/preferences — 전체 설정 조회
PATCH /api/v1/user/preferences — 설정 부분 업데이트
GET /api/v1/user/favorites — 즐겨찾기 목록
POST /api/v1/user/favorites — 즐겨찾기 추가
DELETE /api/v1/user/favorites/{menuId} — 즐겨찾기 삭제
PATCH /api/v1/user/favorites/reorder — 순서 변경
GET /api/v1/user/table-preferences/{pageId} — 페이지별 컬럼 설정
PUT /api/v1/user/table-preferences/{pageId} — 컬럼 설정 저장
```
### Phase 2 (테마/글꼴/사이드바/알림)
```
GET /api/v1/user/preferences — 위와 동일 (theme, font_size 포함)
PATCH /api/v1/user/preferences — 위와 동일
GET /api/v1/user/notification-preferences
PUT /api/v1/user/notification-preferences
```
---
## 이관 전략
### 단계별 마이그레이션
1. **DB 테이블 + API 생성** (백엔드)
2. **Dual-write 패턴 적용** (프론트)
- 저장 시: API 호출 + localStorage 동시 기록
- 읽기 시: API 우선 → localStorage 폴백
3. **안정화 후 localStorage 제거**
### 프론트 전환 패턴 (예시)
```typescript
// createUserStorage → createUserStorageAPI 전환
export function createUserStorageAPI(baseKey: string) {
return {
getItem: async () => {
const res = await fetch(`/api/v1/user/${baseKey}`);
return res.ok ? res.json() : null;
},
setItem: async (value: unknown) => {
await fetch(`/api/v1/user/${baseKey}`, {
method: 'PUT',
body: JSON.stringify(value),
});
},
};
}
```
---
## 우선순위 정리
| 단계 | 대상 | 이유 |
|------|------|------|
| Phase 1 | 즐겨찾기, 테이블 컬럼 | 유저별 분리 이미 되어있어 구조 전환 쉬움, 사용 빈도 높음 |
| Phase 2 | 테마, 글꼴, 사이드바 | 현재 유저 분리 안 됨 → DB 이관하면서 유저별 적용 |
| Phase 3 | 알림 설정 | 기능 안정화 후 진행 |