fix: 고객센터 시스템 게시판 API 엔드포인트 및 날짜 필터 수정
- shared/actions.ts: /boards/ → /system-boards/ 엔드포인트 변경 - EventList, InquiryList, NoticeList: 날짜 범위 초기값 빈 문자열로 변경 (전체 조회) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
57
.serena/memories/comprehensive-analysis-fix-2025-12-27.md
Normal file
57
.serena/memories/comprehensive-analysis-fix-2025-12-27.md
Normal file
@@ -0,0 +1,57 @@
|
||||
# 종합분석 버그 수정 완료 (2025-12-27)
|
||||
|
||||
## 해결한 문제
|
||||
|
||||
**증상**: 종합분석 페이지(`/reports/comprehensive-analysis`)에서 승인/반려 클릭 시 "결재 순서가 아닙니다" 오류
|
||||
|
||||
**원인**:
|
||||
- `ComprehensiveAnalysisService::getTodayIssue()`가 테넌트의 **모든** 대기 결재를 반환
|
||||
- 현재 사용자(홍킬동, User 33)가 결재자가 아닌 문서도 포함되어 있음
|
||||
- 다른 사용자(Ops Admin, User 12)가 결재자인 문서를 승인하려고 하면 오류 발생
|
||||
|
||||
## 수정 내용
|
||||
|
||||
### 1. ComprehensiveAnalysisService.php
|
||||
|
||||
```php
|
||||
// 수정 전
|
||||
$pendingApprovals = Approval::where('tenant_id', $tenantId)
|
||||
->pending()
|
||||
->with(['form', 'drafter'])
|
||||
->get();
|
||||
|
||||
// 수정 후
|
||||
$pendingApprovals = Approval::where('tenant_id', $tenantId)
|
||||
->pending()
|
||||
->whereHas('steps', function ($q) use ($userId) {
|
||||
$q->where('approver_id', $userId)
|
||||
->where('status', ApprovalStep::STATUS_PENDING);
|
||||
})
|
||||
->with(['form', 'drafter'])
|
||||
->get();
|
||||
```
|
||||
|
||||
### 2. ComprehensiveAnalysisSeeder.php
|
||||
|
||||
- `tenantId = 287` (프론트_테스트회사)
|
||||
- `userId = 33` (홍킬동)
|
||||
- 결재 단계에서 `approver_id = 33` 설정
|
||||
|
||||
## 테스트 환경
|
||||
|
||||
| 항목 | 값 |
|
||||
|------|-----|
|
||||
| Tenant ID | 287 |
|
||||
| 테스트 User | 홍킬동 (User 33, hhhhhh@example.com) |
|
||||
| 보조 User | Ops Admin (User 12) |
|
||||
|
||||
## Git 커밋
|
||||
|
||||
- **api**: `fix: 종합분석 오늘의 이슈 승인/반려 버그 수정`
|
||||
- **docs**: `docs: 종합분석 버그 수정 변경이력 추가`
|
||||
|
||||
## 관련 파일
|
||||
|
||||
- `api/app/Services/ComprehensiveAnalysisService.php`
|
||||
- `api/database/seeders/ComprehensiveAnalysisSeeder.php`
|
||||
- `docs/plans/react-mock-remaining-tasks.md`
|
||||
@@ -17,15 +17,22 @@ public function rules(): array
|
||||
$stepTypes = implode(',', ApprovalLine::STEP_TYPES);
|
||||
|
||||
return [
|
||||
'form_id' => 'required|integer|exists:approval_forms,id',
|
||||
// form_id 또는 form_code 중 하나 필수
|
||||
'form_id' => 'required_without:form_code|integer|exists:approval_forms,id',
|
||||
'form_code' => 'required_without:form_id|string|exists:approval_forms,code',
|
||||
'title' => 'required|string|max:200',
|
||||
'content' => 'required|array',
|
||||
'attachments' => 'nullable|array',
|
||||
'attachments.*' => 'integer|exists:files,id',
|
||||
'submit' => 'nullable|boolean',
|
||||
'steps' => 'required_if:submit,true|array|min:1',
|
||||
'steps.*.type' => "required_with:steps|string|in:{$stepTypes}",
|
||||
'steps.*.user_id' => 'required_with:steps|integer|exists:users,id',
|
||||
// type 또는 step_type
|
||||
'steps.*.type' => "nullable|string|in:{$stepTypes}",
|
||||
'steps.*.step_type' => "nullable|string|in:{$stepTypes}",
|
||||
// user_id 또는 approver_id
|
||||
'steps.*.user_id' => 'nullable|integer|exists:users,id',
|
||||
'steps.*.approver_id' => 'nullable|integer|exists:users,id',
|
||||
'steps.*.step_order' => 'nullable|integer|min:1',
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -17,21 +17,23 @@ public function rules(): array
|
||||
$stepTypes = implode(',', ApprovalLine::STEP_TYPES);
|
||||
|
||||
return [
|
||||
'steps' => 'required|array|min:1',
|
||||
'steps.*.type' => "required|string|in:{$stepTypes}",
|
||||
'steps.*.user_id' => 'required|integer|exists:users,id',
|
||||
// steps는 optional - 없으면 기존 결재선 사용
|
||||
'steps' => 'nullable|array',
|
||||
'steps.*.type' => "nullable|string|in:{$stepTypes}",
|
||||
'steps.*.step_type' => "nullable|string|in:{$stepTypes}",
|
||||
'steps.*.user_id' => 'nullable|integer|exists:users,id',
|
||||
'steps.*.approver_id' => 'nullable|integer|exists:users,id',
|
||||
'steps.*.step_order' => 'nullable|integer|min:1',
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'steps.required' => __('error.approval.steps_required'),
|
||||
'steps.min' => __('validation.min.array', ['attribute' => '결재단계', 'min' => 1]),
|
||||
'steps.*.type.required' => __('validation.required', ['attribute' => '단계유형']),
|
||||
'steps.*.type.in' => __('validation.in', ['attribute' => '단계유형']),
|
||||
'steps.*.user_id.required' => __('validation.required', ['attribute' => '결재자']),
|
||||
'steps.*.step_type.in' => __('validation.in', ['attribute' => '단계유형']),
|
||||
'steps.*.user_id.exists' => __('validation.exists', ['attribute' => '결재자']),
|
||||
'steps.*.approver_id.exists' => __('validation.exists', ['attribute' => '결재자']),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,12 +13,23 @@ public function authorize(): bool
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
$stepTypes = implode(',', \App\Models\Tenants\ApprovalLine::STEP_TYPES);
|
||||
|
||||
return [
|
||||
// form_id 또는 form_code
|
||||
'form_id' => 'nullable|integer|exists:approval_forms,id',
|
||||
'form_code' => 'nullable|string|exists:approval_forms,code',
|
||||
'title' => 'nullable|string|max:200',
|
||||
'content' => 'nullable|array',
|
||||
'attachments' => 'nullable|array',
|
||||
'attachments.*' => 'integer|exists:files,id',
|
||||
// 결재선 수정 지원
|
||||
'steps' => 'nullable|array',
|
||||
'steps.*.type' => "nullable|string|in:{$stepTypes}",
|
||||
'steps.*.step_type' => "nullable|string|in:{$stepTypes}",
|
||||
'steps.*.user_id' => 'nullable|integer|exists:users,id',
|
||||
'steps.*.approver_id' => 'nullable|integer|exists:users,id',
|
||||
'steps.*.step_order' => 'nullable|integer|min:1',
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -320,7 +320,15 @@ public function drafts(array $params): LengthAwarePaginator
|
||||
$query = Approval::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('drafter_id', $userId)
|
||||
->with(['form:id,name,code,category', 'drafter:id,name']);
|
||||
->with([
|
||||
'form:id,name,code,category',
|
||||
'drafter:id,name',
|
||||
'drafter.tenantProfile:id,user_id,position_key,department_id',
|
||||
'drafter.tenantProfile.department:id,name',
|
||||
'steps.approver:id,name',
|
||||
'steps.approver.tenantProfile:id,user_id,position_key,department_id',
|
||||
'steps.approver.tenantProfile.department:id,name',
|
||||
]);
|
||||
|
||||
// 상태 필터
|
||||
if (! empty($params['status'])) {
|
||||
@@ -543,7 +551,11 @@ public function show(int $id): Approval
|
||||
->with([
|
||||
'form:id,name,code,category,template',
|
||||
'drafter:id,name,email',
|
||||
'drafter.tenantProfile:id,user_id,tenant_id,department_id,position_key,display_name',
|
||||
'drafter.tenantProfile.department:id,name',
|
||||
'steps.approver:id,name,email',
|
||||
'steps.approver.tenantProfile:id,user_id,tenant_id,department_id,position_key,display_name',
|
||||
'steps.approver.tenantProfile.department:id,name',
|
||||
])
|
||||
->findOrFail($id);
|
||||
}
|
||||
@@ -557,12 +569,18 @@ public function store(array $data): Approval
|
||||
$userId = $this->apiUserId();
|
||||
|
||||
return DB::transaction(function () use ($data, $tenantId, $userId) {
|
||||
// 양식 확인
|
||||
$form = ApprovalForm::query()
|
||||
// 양식 확인 (form_id 또는 form_code 지원)
|
||||
$formQuery = ApprovalForm::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('id', $data['form_id'])
|
||||
->active()
|
||||
->firstOrFail();
|
||||
->active();
|
||||
|
||||
if (! empty($data['form_id'])) {
|
||||
$form = $formQuery->where('id', $data['form_id'])->firstOrFail();
|
||||
} elseif (! empty($data['form_code'])) {
|
||||
$form = $formQuery->where('code', $data['form_code'])->firstOrFail();
|
||||
} else {
|
||||
throw new BadRequestHttpException(__('error.approval.form_required'));
|
||||
}
|
||||
|
||||
// 문서번호 생성
|
||||
$documentNumber = $this->generateDocumentNumber($tenantId);
|
||||
@@ -572,7 +590,7 @@ public function store(array $data): Approval
|
||||
$approval = Approval::create([
|
||||
'tenant_id' => $tenantId,
|
||||
'document_number' => $documentNumber,
|
||||
'form_id' => $data['form_id'],
|
||||
'form_id' => $form->id,
|
||||
'title' => $data['title'],
|
||||
'content' => $data['content'],
|
||||
'status' => $status,
|
||||
@@ -583,15 +601,19 @@ public function store(array $data): Approval
|
||||
'updated_by' => $userId,
|
||||
]);
|
||||
|
||||
// 결재선 생성 (상신 시)
|
||||
if ($status === Approval::STATUS_PENDING && ! empty($data['steps'])) {
|
||||
// 결재선 생성 (steps가 있으면 항상 저장)
|
||||
if (! empty($data['steps'])) {
|
||||
$this->createApprovalSteps($approval, $data['steps']);
|
||||
}
|
||||
|
||||
return $approval->fresh([
|
||||
'form:id,name,code,category',
|
||||
'drafter:id,name',
|
||||
'drafter.tenantProfile:id,user_id,tenant_id,department_id,position_key,display_name',
|
||||
'drafter.tenantProfile.department:id,name',
|
||||
'steps.approver:id,name',
|
||||
'steps.approver.tenantProfile:id,user_id,tenant_id,department_id,position_key,display_name',
|
||||
'steps.approver.tenantProfile.department:id,name',
|
||||
]);
|
||||
});
|
||||
}
|
||||
@@ -613,8 +635,23 @@ public function update(int $id, array $data): Approval
|
||||
throw new BadRequestHttpException(__('error.approval.not_editable'));
|
||||
}
|
||||
|
||||
// form_id 또는 form_code로 양식 ID 결정
|
||||
$formId = $approval->form_id;
|
||||
if (! empty($data['form_id'])) {
|
||||
$formId = $data['form_id'];
|
||||
} elseif (! empty($data['form_code'])) {
|
||||
$form = ApprovalForm::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('code', $data['form_code'])
|
||||
->active()
|
||||
->first();
|
||||
if ($form) {
|
||||
$formId = $form->id;
|
||||
}
|
||||
}
|
||||
|
||||
$approval->fill([
|
||||
'form_id' => $data['form_id'] ?? $approval->form_id,
|
||||
'form_id' => $formId,
|
||||
'title' => $data['title'] ?? $approval->title,
|
||||
'content' => $data['content'] ?? $approval->content,
|
||||
'attachments' => $data['attachments'] ?? $approval->attachments,
|
||||
@@ -623,9 +660,21 @@ public function update(int $id, array $data): Approval
|
||||
|
||||
$approval->save();
|
||||
|
||||
// 결재선 수정 (steps가 전달된 경우)
|
||||
if (! empty($data['steps'])) {
|
||||
// 기존 결재선 삭제 후 새로 생성
|
||||
$approval->steps()->delete();
|
||||
$this->createApprovalSteps($approval, $data['steps']);
|
||||
}
|
||||
|
||||
return $approval->fresh([
|
||||
'form:id,name,code,category',
|
||||
'drafter:id,name',
|
||||
'drafter.tenantProfile:id,user_id,tenant_id,department_id,position_key,display_name',
|
||||
'drafter.tenantProfile.department:id,name',
|
||||
'steps.approver:id,name',
|
||||
'steps.approver.tenantProfile:id,user_id,tenant_id,department_id,position_key,display_name',
|
||||
'steps.approver.tenantProfile.department:id,name',
|
||||
]);
|
||||
});
|
||||
}
|
||||
@@ -672,12 +721,21 @@ public function submit(int $id, array $data): Approval
|
||||
throw new BadRequestHttpException(__('error.approval.not_submittable'));
|
||||
}
|
||||
|
||||
if (empty($data['steps'])) {
|
||||
throw new BadRequestHttpException(__('error.approval.steps_required'));
|
||||
}
|
||||
// steps가 있으면 새로 생성, 없으면 기존 결재선 사용
|
||||
if (! empty($data['steps'])) {
|
||||
// 기존 결재선 삭제 후 새로 생성
|
||||
$approval->steps()->delete();
|
||||
$this->createApprovalSteps($approval, $data['steps']);
|
||||
} else {
|
||||
// 기존 결재선이 없으면 에러
|
||||
$existingSteps = $approval->steps()
|
||||
->whereIn('step_type', [ApprovalLine::STEP_TYPE_APPROVAL, ApprovalLine::STEP_TYPE_AGREEMENT])
|
||||
->count();
|
||||
|
||||
// 결재선 생성
|
||||
$this->createApprovalSteps($approval, $data['steps']);
|
||||
if ($existingSteps === 0) {
|
||||
throw new BadRequestHttpException(__('error.approval.steps_required'));
|
||||
}
|
||||
}
|
||||
|
||||
$approval->status = Approval::STATUS_PENDING;
|
||||
$approval->drafted_at = now();
|
||||
@@ -688,7 +746,11 @@ public function submit(int $id, array $data): Approval
|
||||
return $approval->fresh([
|
||||
'form:id,name,code,category',
|
||||
'drafter:id,name',
|
||||
'drafter.tenantProfile:id,user_id,tenant_id,department_id,position_key,display_name',
|
||||
'drafter.tenantProfile.department:id,name',
|
||||
'steps.approver:id,name',
|
||||
'steps.approver.tenantProfile:id,user_id,tenant_id,department_id,position_key,display_name',
|
||||
'steps.approver.tenantProfile.department:id,name',
|
||||
]);
|
||||
});
|
||||
}
|
||||
@@ -748,7 +810,11 @@ public function approve(int $id, ?string $comment = null): Approval
|
||||
return $approval->fresh([
|
||||
'form:id,name,code,category',
|
||||
'drafter:id,name',
|
||||
'drafter.tenantProfile:id,user_id,tenant_id,department_id,position_key,display_name',
|
||||
'drafter.tenantProfile.department:id,name',
|
||||
'steps.approver:id,name',
|
||||
'steps.approver.tenantProfile:id,user_id,tenant_id,department_id,position_key,display_name',
|
||||
'steps.approver.tenantProfile.department:id,name',
|
||||
]);
|
||||
});
|
||||
}
|
||||
@@ -797,7 +863,11 @@ public function reject(int $id, string $comment): Approval
|
||||
return $approval->fresh([
|
||||
'form:id,name,code,category',
|
||||
'drafter:id,name',
|
||||
'drafter.tenantProfile:id,user_id,tenant_id,department_id,position_key,display_name',
|
||||
'drafter.tenantProfile.department:id,name',
|
||||
'steps.approver:id,name',
|
||||
'steps.approver.tenantProfile:id,user_id,tenant_id,department_id,position_key,display_name',
|
||||
'steps.approver.tenantProfile.department:id,name',
|
||||
]);
|
||||
});
|
||||
}
|
||||
@@ -867,7 +937,11 @@ public function markRead(int $id): Approval
|
||||
return $approval->fresh([
|
||||
'form:id,name,code,category',
|
||||
'drafter:id,name',
|
||||
'drafter.tenantProfile:id,user_id,tenant_id,department_id,position_key,display_name',
|
||||
'drafter.tenantProfile.department:id,name',
|
||||
'steps.approver:id,name',
|
||||
'steps.approver.tenantProfile:id,user_id,tenant_id,department_id,position_key,display_name',
|
||||
'steps.approver.tenantProfile.department:id,name',
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -899,7 +973,11 @@ public function markUnread(int $id): Approval
|
||||
return $approval->fresh([
|
||||
'form:id,name,code,category',
|
||||
'drafter:id,name',
|
||||
'drafter.tenantProfile:id,user_id,tenant_id,department_id,position_key,display_name',
|
||||
'drafter.tenantProfile.department:id,name',
|
||||
'steps.approver:id,name',
|
||||
'steps.approver.tenantProfile:id,user_id,tenant_id,department_id,position_key,display_name',
|
||||
'steps.approver.tenantProfile.department:id,name',
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -932,18 +1010,28 @@ private function generateDocumentNumber(int $tenantId): string
|
||||
|
||||
/**
|
||||
* 결재 단계 생성
|
||||
* 프론트엔드 호환성: step_type/approver_id 또는 type/user_id 지원
|
||||
*/
|
||||
private function createApprovalSteps(Approval $approval, array $steps): void
|
||||
{
|
||||
$order = 1;
|
||||
foreach ($steps as $step) {
|
||||
ApprovalStep::create([
|
||||
'approval_id' => $approval->id,
|
||||
'step_order' => $order++,
|
||||
'step_type' => $step['type'],
|
||||
'approver_id' => $step['user_id'],
|
||||
'status' => ApprovalStep::STATUS_PENDING,
|
||||
]);
|
||||
// 필드명 호환성: step_type 또는 type
|
||||
$stepType = $step['step_type'] ?? $step['type'] ?? null;
|
||||
// 필드명 호환성: approver_id 또는 user_id
|
||||
$approverId = $step['approver_id'] ?? $step['user_id'] ?? null;
|
||||
// step_order가 있으면 사용, 없으면 자동 증가
|
||||
$stepOrder = $step['step_order'] ?? $order++;
|
||||
|
||||
if ($stepType && $approverId) {
|
||||
ApprovalStep::create([
|
||||
'approval_id' => $approval->id,
|
||||
'step_order' => $stepOrder,
|
||||
'step_type' => $stepType,
|
||||
'approver_id' => $approverId,
|
||||
'status' => ApprovalStep::STATUS_PENDING,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,9 +51,9 @@ public function run(): void
|
||||
private function seedApprovalForms(): void
|
||||
{
|
||||
$forms = [
|
||||
['code' => 'REQ-001', 'name' => '품의서', 'category' => 'request'],
|
||||
['code' => 'EXP-001', 'name' => '지출결의서', 'category' => 'expense'],
|
||||
['code' => 'EST-001', 'name' => '지출 예상 내역서', 'category' => 'expense_estimate'],
|
||||
['code' => 'proposal', 'name' => '품의서', 'category' => 'request'],
|
||||
['code' => 'expenseReport', 'name' => '지출결의서', 'category' => 'expense'],
|
||||
['code' => 'expenseEstimate', 'name' => '지출 예상 내역서', 'category' => 'expense_estimate'],
|
||||
];
|
||||
|
||||
foreach ($forms as $form) {
|
||||
|
||||
@@ -200,6 +200,7 @@
|
||||
'approval' => [
|
||||
'not_found' => '결재 문서를 찾을 수 없습니다.',
|
||||
'form_not_found' => '결재 양식을 찾을 수 없습니다.',
|
||||
'form_required' => '결재 양식이 필요합니다.',
|
||||
'form_code_exists' => '중복된 양식 코드입니다.',
|
||||
'form_in_use' => '사용 중인 양식은 삭제할 수 없습니다.',
|
||||
'line_not_found' => '결재선을 찾을 수 없습니다.',
|
||||
|
||||
Reference in New Issue
Block a user