# SR System — Feature Implementation Plan
> Analysis date: 2026-05-26
> Scope: Calendar, Notifications, User Profile, ADMIN/MANAGER Management
---
## Executive Summary
| Feature | Existing Backend | New Backend | Frontend | Priority | Effort |
|---------|:---:|:---:|:---:|:---:|:---:|
| Calendar | **None** | Calendar aggregation endpoint | CalendarPage + CalendarWidget | P2 | 5d |
| Notifications | **Full** (read, mark-read, unread count) | Add `link_url` field to Notification entity | NotificationDropdown | P1 | 2d |
| User Profile | **Partial** (getMe, portfolio, dispatch-history) | ChangePassword endpoint + Extended EmployeeSummary | ProfilePage | P1 | 4d |
| ADMIN/MANAGER | **Partial** (employee CRUD, report listing) | Dispatch CRUD, CommonCode CRUD, Report summary aggregations | EmployeeEditModal, DispatchManager, ReportDashboard, SettingsPage | P1 (Reports), P2 (Dispatch), P2 (Settings) | 8d total |
**Total estimated effort: 19 days** (full-time, single dev). Parallel work possible — Admin features and Profile can be developed concurrently with Calendar and Notifications.
---
## 1. Calendar Feature
### 1.1 Current State Analysis
| Aspect | Status |
|--------|--------|
| Header Calendar icon | Route: `/issues` (no calendar relevance) |
| Backend calendar endpoint | **Does not exist** |
| Data with dates | `Ticket.dueDate`, `Report.targetDate`, `Project.startDate/endDate`, `DispatchHistory.startDate/endDate` |
| Frontend calendar page | **Does not exist** |
The Header component (line 65):
```tsx
```
### 1.2 What's Missing
1. **Backend: Calendar aggregation endpoint** — No single endpoint to fetch events across domains. Need:
- `GET /api/v1/calendar?start=YYYY-MM-DD&end=YYYY-MM-DD` — returns unified events
- Events aggregated from: Ticket due dates, Report target dates, Dispatch start/end dates
2. **Backend: Calendar event entity** — No dedicated schedule/schedule_type entity. Two options:
- **Option A (Recommended):** Aggregation endpoint that queries existing entities (Ticket, Report, DispatchHistory) and returns a unified DTO. Zero schema change.
- **Option B:** New `Schedule` entity with schedule_type (MEETING, DEADLINE, MILESTONE) — more flexible but adds complexity.
3. **Frontend: Calendar page** (`/app/(main)/calendar/page.tsx`) — Full calendar view with month/week/day modes.
4. **Frontend: Calendar event types** — Ticket due dates, report deadlines, dispatch milestones.
### 1.3 Design Proposal
#### 1.3.1 Backend: Calendar Aggregation Endpoint
**New DTO: `CalendarEventDto`**
```java
public record CalendarEventDto(
String eventType, // TICKET_DUE, REPORT_DUE, DISPATCH_START, DISPATCH_END
Long sourceId, // ticketId / reportId / dispatchId
String title,
String description,
LocalDate eventDate,
String color, // semantic color per type
String linkUrl // navigation URL (/issues/123, /work-reports/456, etc.)
) {}
```
**New Controller Method (in a new `CalendarController` or added to an existing controller):**
```java
@GetMapping("/api/v1/calendar")
@PreAuthorize("hasAnyRole('EMPLOYEE','MANAGER','ADMIN')")
ResponseEntity>> getCalendar(
@RequestParam @DateTimeFormat(iso=ISO.DATE) LocalDate start,
@RequestParam @DateTimeFormat(iso=ISO.DATE) LocalDate end,
@CurrentUser Employee currentUser)
```
**New Service: `CalendarService`**
- Queries `TicketRepository.findByDueDateBetweenAndRequester_EmpId(start, end, empId)` (for employee) or all tickets (for manager/admin)
- Queries `ReportRepository.findByTargetDateBetweenAndUser_EmpId(start, end, empId)` or all
- Queries `DispatchHistoryRepository.findByStartDateBetween(start, end)` (only active, DISPATCHED)
- Merges results into `List`
**Event color mapping:**
| Event Type | Color | Icon |
|-----------|-------|------|
| TICKET_DUE | `#3B82F6` (blue) | Ticket |
| REPORT_DUE | `#10B981` (green) | FileText |
| DISPATCH_START | `#F59E0B` (amber) | Play |
| DISPATCH_END | `#EF4444` (red) | Stop |
#### 1.3.2 Frontend: Calendar Page
**Route:** `/calendar`
**Design:**
```
┌─────────────────────────────────────────────────────────────────┐
│ Calendar ◀ 2026.05 ▶ Today │
├──┬────┬────┬────┬────┬────┬────┐ │
│S │ M │ T │ W │ T │ F │ S │ │
├──┼────┼────┼────┼────┼────┼────┤ │
│ │ │ │ │ │ 1 │ 2 │ │
│ │ │ │ │ │ ● │ │ │
│ │ 4 │ 5 │ 6 │ 7 │ 8 │ 9 │ │
│ │ ● │ ● │ │ ■ │ │ │ │
│ │ 11 │ 12 │ 13 │ 14 │ 15 │ 16 │ │
│ │ │ ▲ │ ● │ │ │ ■ │ │
│ │ 18 │ 19 │ 20 │ 21 │ 22 │ 23 │ │
│ │ │ │ ● │ │ │ │ │
│ │ 25 │ 26 │ 27 │ 28 │ 29 │ 30 │ │
│ │ │ │ ■ │ │ │ │ │
├──┴────┴────┴────┴────┴────┴────┤ │
│ Legend: ● Report Due ■ Dispatch ▲ Ticket Due │
└─────────────────────────────────────────────────────────────────┘
```
**Components to create:**
- `src/app/(main)/calendar/page.tsx` — Main calendar page
- `src/components/calendar/CalendarGrid.tsx` — Month grid view (no external library — custom CSS Grid, 7-column)
- `src/components/calendar/CalendarEvent.tsx` — Event chip with color dot + title (truncated)
- `src/components/calendar/EventDetailPanel.tsx` — Slide-over or modal showing event details
- `src/components/calendar/CalendarWidget.tsx` — Mini calendar widget for dashboard (optional, P3)
**Added to sidebar:**
```tsx
{ name: 'Calendar', href: '/calendar', icon: Calendar }
```
### 1.4 Implementation Plan — Calendar
| Step | Task | File(s) | Effort |
|------|------|---------|--------|
| 1 | Create `CalendarEventDto` record | `backend/.../dto/CalendarEventDto.java` | 0.3d |
| 2 | Create `CalendarService` with aggregation logic | `backend/.../service/CalendarService.java` | 0.7d |
| 3 | Create `CalendarController` endpoint | `backend/.../controller/CalendarController.java` | 0.3d |
| 4 | Add `calendarService` to frontend services | `src/services/calendarService.ts` | 0.3d |
| 5 | Add `CalendarEvent` type to frontend types | `src/types/index.ts` | 0.2d |
| 6 | Build `CalendarGrid` component | `src/components/calendar/CalendarGrid.tsx` | 1.5d |
| 7 | Build `CalendarEvent` chip component | `src/components/calendar/CalendarEvent.tsx` | 0.5d |
| 8 | Build `EventDetailPanel` slide-over | `src/components/calendar/EventDetailPanel.tsx` | 0.5d |
| 9 | Build `/calendar` page | `src/app/(main)/calendar/page.tsx` | 0.5d |
| 10 | Update Header Calendar icon to `/calendar` | `src/components/layout/Header.tsx` | 0.1d |
| 11 | Add Calendar to Sidebar | `src/components/layout/Sidebar.tsx` | 0.1d |
| **Total** | | | **5 days** |
---
## 2. Notification System
### 2.1 Current State Analysis
| Aspect | Status |
|--------|--------|
| Backend API | ✅ Full: `GET /notifications`, `GET /notifications/unread`, `PUT /notifications/{id}/read` |
| Frontend notificationStore | ✅ Polls unread count (30s interval), fetches list, marks as read |
| Notification entity fields | `notificationId, type, title, message, isRead, createdAt` |
| Bell icon behavior | Calls `fetchNotifications()` on click but **no dropdown/modal renders** |
| Missing | `link_url` field — no way to navigate to related entity from notification |
### 2.2 What's Missing
1. **Frontend: NotificationDropdown component** — Bell click should show a dropdown listing recent notifications.
2. **Frontend: Click-to-navigate** — Clicking a notification should navigate to the relevant page (ticket detail, report, etc.).
3. **Backend: `linkUrl` field** — Notification entity has `type` but no link to the source entity.
### 2.3 Design Proposal
#### 2.3.1 Backend: Add linkUrl to Notification Entity
**Migration:**
```sql
ALTER TABLE tb_notification ADD COLUMN link_url VARCHAR(500);
```
**Entity update (`Notification.java`):**
```java
@Column(name = "link_url", length = 500)
private String linkUrl;
```
**Service update** — When creating notifications, set `linkUrl`:
| Type | linkUrl Example |
|------|----------------|
| TICKET_ASSIGNED | `/issues/123` |
| TICKET_RESOLVED | `/issues/123` |
| REPORT_APPROVED | `/work-reports/456` |
| REPORT_REJECTED | `/work-reports/456` |
| SLA_WARNING | `/issues/789` |
| EQUIPMENT_ASSIGNED | `/itam/123` |
#### 2.3.2 Frontend: Notification Dropdown
**Component: `NotificationDropdown.tsx`**
```
┌─────────────────────────────────┐
│ Notifications [x] │ ← Header: "Notifications" + close button
├─────────────────────────────────┤
│ 🔵 Ticket #123 assigned to you │ ← Unread: blue left border
│ 2 minutes ago → │
├─────────────────────────────────┤
│ ✅ Report #456 approved │ ← Read: no left border
│ 1 hour ago → │
├─────────────────────────────────┤
│ ⚠️ SLA Warning: #789 Overdue │ ← Critical: red left border
│ 3 hours ago → │
├─────────────────────────────────┤
│ │
│ View All ↗ │ ← Link to full notification page
└─────────────────────────────────┘
```
**Behavior:**
- Dropdown slides down from bell icon (absolute positioned)
- Shows latest 10 notifications
- Click notification → `markAsRead(id)` + navigate to `linkUrl`
- Click "View All" → `/notifications` page (optional, P3)
- Click outside → close dropdown
- Escape key → close dropdown
**Layout:**
- Max height: 400px, overflow-y: auto
- Each item: 12px padding, border-bottom, hover bg-gray-50
- Width: 360px
- Unread items: `bg-indigo-50/30`, blue dot (6px) left
- Categories: Icon + title (bold 13px) + time ago (gray 11px)
### 2.4 Implementation Plan — Notifications
| Step | Task | File(s) | Effort |
|------|------|---------|--------|
| 1 | Add `linkUrl` column to Notification entity | `Notification.java` + migration | 0.3d |
| 2 | Update NotificationListener to set linkUrl | `backend/.../listener/NotificationListener.java` | 0.3d |
| 3 | Add `linkUrl` to frontend Notification type | `src/types/index.ts` | 0.1d |
| 4 | Build `NotificationDropdown` component | `src/components/layout/NotificationDropdown.tsx` | 1.0d |
| 5 | Integrate into Header (replace bell onClick) | `src/components/layout/Header.tsx` | 0.2d |
| 6 | Add mark-all-as-read endpoint (optional) | Backend + service | 0.2d |
| **Total** | | | **2 days** |
---
## 3. User Profile Management
### 3.1 Current State Analysis
| Aspect | Status |
|--------|--------|
| `GET /api/v1/employees/me` | ✅ Returns `EmployeeSummary` (empId, name, email, roleCode, dispatchStatus) |
| `GET /api/v1/employees/{empId}/dispatch-histories` | ✅ Returns `DispatchHistory[]` |
| `GET /api/v1/employees/{empId}/portfolio` | ✅ Returns `PortfolioResponse[]` (with reports + resolved tickets) |
| `PUT /api/v1/employees/{empId}` | ✅ Admin-only update endpoint |
| Change password endpoint | **Missing** |
| Extended profile info (phone, empStatus, joinedDate) | **Missing** from `/me` (EmployeeSummary is minimal) |
| `/profile` page | **Does not exist** |
### 3.2 What's Missing
1. **Backend: `PUT /api/v1/employees/me/password`** — Change password endpoint for current user.
2. **Backend: Extended `/me` response** — `EmployeeSummary` is too minimal. Need phone, empStatus, joinedDate, dispatchStatus label.
3. **Frontend: `/profile` page** — View/edit profile, change password, view dispatch history, view portfolio.
4. **Frontend: Profile editing** — Allow user to edit their own name, email, phone (or restrict to admin-only for certain fields).
### 3.3 Design Proposal
#### 3.3.1 Backend: Change Password Endpoint
**New DTO: `ChangePasswordRequest`**
```java
public record ChangePasswordRequest(
@NotBlank String currentPassword,
@NotBlank @Size(min=8) String newPassword
) {}
```
**New Endpoint in AuthController or EmployeeController:**
```java
@PutMapping("/api/v1/employees/me/password")
@PreAuthorize("hasAnyRole('EMPLOYEE','MANAGER','ADMIN')")
ResponseEntity> changePassword(
@Valid @RequestBody ChangePasswordRequest request,
@CurrentUser Employee currentUser)
```
**Logic:**
1. Verify `currentPassword` matches stored hash
2. Encode `newPassword` and save
3. Invalidate existing tokens (optional — add `token_version` field)
#### 3.3.2 Backend: Extended EmployeeSummary
**Update `EmployeeSummary` to include:**
```java
public record EmployeeSummary(
Long empId,
String loginId,
String name,
String email,
String phone, // NEW
RoleCode roleCode,
EmpStatus empStatus, // NEW
DispatchStatus dispatchStatus,
LocalDate joinedDate, // NEW
LocalDate leftDate // NEW (nullable)
) {}
```
#### 3.3.3 Frontend: Profile Page Design
**URL:** `/profile`
**For EMPLOYEE (own profile):**
```
┌─────────────────────────────────────────────────────┐
│ [Avatar] 김민수 │
│ 파견직원 · 재직중 · 파견중 │
│ │
│ ┌─ Personal Info ─────────────────────────────────┐ │
│ │ Login ID: minsu.kim │ │
│ │ Name: 김민수 │ │
│ │ Email: minsu@example.com │ │
│ │ Phone: 010-1234-5678 │ │
│ │ Joined: 2024-03-01 │ │
│ │ Status: ACTIVE (DISPATCHED) │ │
│ │ [Edit Info] [Change Password] │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ ┌─ Dispatch History ──────────────────────────────┐ │
│ │ Timeline with projects, dates, roles │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ ┌─ Portfolio Summary ─────────────────────────────┐ │
│ │ Aggregated stats: projects, tickets, reports │ │
│ └──────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘
```
**For MANAGER/ADMIN viewing another employee:**
- URL: `/profile/[empId]`
- Same layout but with employee selector/dropdown at top
- Manager can view but not edit (unless admin)
- Admin can edit fields
#### 3.3.4 Components to Create
| Component | Description |
|-----------|-------------|
| `src/app/(main)/profile/page.tsx` | Own profile view |
| `src/app/(main)/profile/[empId]/page.tsx` | View other employee's profile (MANAGER/ADMIN) |
| `src/components/profile/ProfileCard.tsx` | Personal info card with edit/change-password buttons |
| `src/components/profile/EditProfileModal.tsx` | Modal for editing personal info (admin only or self?) |
| `src/components/profile/ChangePasswordModal.tsx` | Modal with current/new/confirm password fields |
| `src/components/profile/DispatchTimeline.tsx` | Reuse timeline from /dispatch page |
| `src/components/profile/PortfolioStats.tsx` | Summary stats cards (total projects, total tickets resolved, etc.) |
### 3.4 Implementation Plan — User Profile
| Step | Task | File(s) | Effort |
|------|------|---------|--------|
| 1 | Create `ChangePasswordRequest` DTO | Backend dto | 0.1d |
| 2 | Add `changePassword` to AuthService | `AuthService.java` | 0.3d |
| 3 | Add `PUT /employees/me/password` endpoint | `AuthController.java` or `EmployeeController.java` | 0.2d |
| 4 | Extend `EmployeeSummary` to include phone, empStatus, joinedDate | `EmployeeSummary.java` + `EmployeeService.toSummary()` | 0.2d |
| 5 | Add `getMeFull()` and `changePassword()` to employeeService | `src/services/employeeService.ts` | 0.2d |
| 6 | Build `ProfileCard` component | `src/components/profile/ProfileCard.tsx` | 0.5d |
| 7 | Build `ChangePasswordModal` component | `src/components/profile/ChangePasswordModal.tsx` | 0.5d |
| 8 | Build `EditProfileModal` component (admin-only for role/status changes; self-service for email/phone) | `src/components/profile/EditProfileModal.tsx` | 0.5d |
| 9 | Build `PortfolioStats` component | `src/components/profile/PortfolioStats.tsx` | 0.3d |
| 10 | Build `/profile` page | `src/app/(main)/profile/page.tsx` | 0.8d |
| 11 | Build `/profile/[empId]` page (manager/admin view) | `src/app/(main)/profile/[empId]/page.tsx` | 0.4d |
| 12 | Make user avatar/name in Header clickable → `/profile` | `src/components/layout/Header.tsx` | 0.1d |
| **Total** | | | **4 days** |
---
## 4. ADMIN/MANAGER Management Features
### 4.1 Current State Analysis
#### 4.1.1 Employee CRUD
| Operation | Backend | Frontend |
|-----------|:---:|:---:|
| List/View | ✅ `GET /employees` (MANAGER/ADMIN) | ✅ `/employees` page with table view |
| Create | ✅ `POST /employees` (ADMIN) | ✅ Modal in `/employees` page |
| Update | ✅ `PUT /employees/{id}` (ADMIN) | ❌ Only `MoreHorizontal` button with no handler |
| Delete | ✅ `DELETE /employees/{id}` (ADMIN) | ❌ No delete UI |
| Search | ✅ `GET /employees/search` | ✅ Search bar in `/employees` |
**Gap:** Edit and delete functionality missing from frontend. The `MoreHorizontal` button (line 454-456 in employees/page.tsx) is a blank `