Compare commits
389 Commits
develop
...
b67bc20f1b
| Author | SHA1 | Date | |
|---|---|---|---|
| b67bc20f1b | |||
| 9e69c64024 | |||
| 3e1d1ffc33 | |||
|
|
d9be4e2400 | ||
|
|
b708f473d1 | ||
|
|
e95598acad | ||
|
|
8f287149ef | ||
|
|
d43cb4bc9b | ||
|
|
5f1a211722 | ||
|
|
05321c8839 | ||
|
|
f149a987c9 | ||
|
|
33f02379e4 | ||
|
|
bf802d6af3 | ||
|
|
20c4e9d879 | ||
|
|
ae367e733e | ||
|
|
c96a92bcb5 | ||
|
|
da20e3552f | ||
|
|
eeb56ae206 | ||
|
|
3e47402d3e | ||
|
|
3ace66065e | ||
|
|
48a41e535b | ||
|
|
8111910d6c | ||
|
|
11d5fb57a7 | ||
|
|
a164410791 | ||
|
|
3fccd7414c | ||
|
|
08bf255480 | ||
|
|
c8223a63b5 | ||
|
|
a474c26675 | ||
|
|
47fcaaa3a3 | ||
|
|
43862e26c6 | ||
|
|
eb47351af1 | ||
|
|
0649802ffd | ||
|
|
017e2882a9 | ||
|
|
fa96b8e013 | ||
|
|
520406a06c | ||
|
|
2906825c33 | ||
|
|
53fb5103ac | ||
|
|
033e8b12cc | ||
|
|
bf3ec57095 | ||
|
|
7679c2b99b | ||
|
|
12239866db | ||
|
|
406d47bc93 | ||
|
|
cba91713ee | ||
|
|
e02f1daf0a | ||
|
|
36fa02132c | ||
|
|
f7bb375bea | ||
|
|
f83986aec3 | ||
|
|
f00eee7f12 | ||
|
|
b60f2109af | ||
|
|
10b3490d9c | ||
|
|
b05daffedb | ||
|
|
d9c905ca9a | ||
|
|
2dc559d190 | ||
|
|
7c03aba203 | ||
|
|
4daca61007 | ||
|
|
8cdedae07e | ||
|
|
dab120eacd | ||
|
|
cb88c02ae5 | ||
|
|
e7e0f55a27 | ||
|
|
53a851740a | ||
|
|
106e654cbd | ||
|
|
6de7ff21e4 | ||
|
|
8e4be54e3e | ||
|
|
2b98ac56dd | ||
|
|
a96cb35068 | ||
|
|
5ae9db5842 | ||
|
|
e28077745f | ||
|
|
5c24a70a87 | ||
|
|
d5abdfbe6b | ||
|
|
2658b44618 | ||
|
|
72c93a47fa | ||
|
|
7f73c054d5 | ||
|
|
b82cba0cc9 | ||
|
|
62e9a9b8a2 | ||
|
|
f405c690a4 | ||
|
|
dde0acad72 | ||
|
|
5ed34ca27b | ||
|
|
27f520d303 | ||
|
|
b74c8f8930 | ||
|
|
add05c6546 | ||
|
|
d0d5a7acd9 | ||
|
|
b0f821423d | ||
|
|
86fce5e67f | ||
|
|
975a90d1f8 | ||
|
|
1b85a26f1d | ||
|
|
810242ecdf | ||
|
|
a63b501964 | ||
|
|
7b9c101065 | ||
|
|
3f799308ed | ||
|
|
dd9b534162 | ||
|
|
8af1647173 | ||
|
|
013df2592f | ||
|
|
9192291400 | ||
|
|
da1fc41acb | ||
|
|
3bba48e443 | ||
|
|
446d0ff60b | ||
|
|
135d88e812 | ||
|
|
272df31501 | ||
|
|
0e9f1297b8 | ||
|
|
9727a092e6 | ||
|
|
5fd69830ca | ||
|
|
61e77346de | ||
|
|
280367170a | ||
| 999cbad667 | |||
|
|
2e999114ae | ||
|
|
daa7d40f4e | ||
|
|
76aabebc6e | ||
|
|
dcbeafd92c | ||
|
|
29c41165cc | ||
|
|
447c2152d2 | ||
|
|
75b359cec6 | ||
|
|
a29d246330 | ||
|
|
011446bab5 | ||
|
|
8226552da5 | ||
|
|
0d0e458d63 | ||
|
|
7528113fc3 | ||
|
|
eae39be233 | ||
|
|
74d406fceb | ||
|
|
d431fc3637 | ||
|
|
7b81f954d7 | ||
|
|
871b470ff2 | ||
|
|
5c652e6b21 | ||
|
|
fa4132c946 | ||
|
|
33fcec9c9c | ||
|
|
e006f25427 | ||
|
|
087ad1c7b9 | ||
|
|
beff55cabb | ||
|
|
9beda571a4 | ||
|
|
aa8ae86f1d | ||
|
|
0593700e40 | ||
|
|
25b6470555 | ||
|
|
a8b6a781bd | ||
|
|
322442aef6 | ||
|
|
1da6ae7841 | ||
|
|
94ae19e14a | ||
|
|
367b81d504 | ||
|
|
06cd50d1a6 | ||
|
|
6188762f8c | ||
|
|
d77b9615b3 | ||
|
|
fc63ea80ff | ||
|
|
ec388df7b3 | ||
|
|
4a72368107 | ||
|
|
f5e7e6c2a8 | ||
|
|
f8f9619258 | ||
|
|
32e680dce8 | ||
|
|
7ee3c9398a | ||
|
|
3b7e493b19 | ||
|
|
4dc445aaf1 | ||
|
|
f8bfb9dfa6 | ||
|
|
c79e33063e | ||
|
|
4ecd34e767 | ||
|
|
d149af95b7 | ||
|
|
706393ea4b | ||
|
|
38484c464d | ||
|
|
ed2ac18518 | ||
|
|
9f45a82940 | ||
|
|
6b7eb29ebe | ||
|
|
092bcbd66c | ||
|
|
36add4d889 | ||
|
|
6674df1b64 | ||
|
|
e8ea3375ad | ||
|
|
511bfa3ec5 | ||
|
|
81b64f25aa | ||
|
|
614cbaef15 | ||
|
|
9b36052a8f | ||
|
|
8762882b54 | ||
|
|
60291e08f1 | ||
|
|
25795f8612 | ||
|
|
3962d4b35c | ||
|
|
6a5976cc5d | ||
|
|
2ac4c188d5 | ||
|
|
e431ab1fbd | ||
| 68a96e32a7 | |||
| 9281bb64b9 | |||
|
|
099d08e49e | ||
|
|
3216bb98bc | ||
|
|
420b80e45a | ||
|
|
1fa2e0ca34 | ||
|
|
a26e66bb8e | ||
|
|
2e3dc556d1 | ||
|
|
d17b46fe80 | ||
|
|
dda94f4db8 | ||
|
|
44f139f757 | ||
|
|
85ec94f07f | ||
|
|
6b411f173e | ||
|
|
d2cde3d1a7 | ||
|
|
ea14de3814 | ||
|
|
626d634767 | ||
|
|
f94213fb39 | ||
|
|
f7a9575655 | ||
|
|
84befa546d | ||
|
|
9fdbee5f0f | ||
|
|
71d1c6dd85 | ||
|
|
5bdf133194 | ||
|
|
1d9725c666 | ||
|
|
31e9b5d605 | ||
|
|
fce349392d | ||
|
|
143418dc71 | ||
|
|
e1289e0f82 | ||
|
|
d9cd5d3526 | ||
|
|
da334f0ee7 | ||
|
|
bec0df29e1 | ||
|
|
ab042cb132 | ||
|
|
511fe69593 | ||
|
|
43127c9c4f | ||
|
|
974a356f39 | ||
|
|
daaf09bedc | ||
|
|
0cf15923dc | ||
|
|
47578da428 | ||
|
|
f74bd8960b | ||
|
|
0706617463 | ||
|
|
97bdc5fbb3 | ||
|
|
cf5b62ba06 | ||
|
|
b4348d393d | ||
|
|
36992d5c23 | ||
|
|
eba34ed009 | ||
|
|
20e82049a8 | ||
|
|
f5bbec4ce6 | ||
|
|
63bc2f1cab | ||
|
|
947e1d1993 | ||
|
|
080e6b13e5 | ||
|
|
e0980702a6 | ||
|
|
f6803e40d6 | ||
|
|
603767cb0e | ||
|
|
34b8a75b08 | ||
|
|
4aea02b085 | ||
|
|
2971401501 | ||
|
|
5f24d01780 | ||
|
|
ba792a0fcc | ||
|
|
69718e7c18 | ||
|
|
350394820b | ||
|
|
b6b16fcbd1 | ||
|
|
ee1a2d6633 | ||
|
|
215aa2bec4 | ||
|
|
d1911265f4 | ||
|
|
9d939a6a6a | ||
|
|
b4283ccf85 | ||
|
|
2816a6b4f4 | ||
|
|
cfdb1044fb | ||
|
|
40534498b3 | ||
|
|
b486dbdc5e | ||
|
|
81f33978af | ||
|
|
c58ca65dc7 | ||
|
|
60aef7992b | ||
|
|
090275e133 | ||
|
|
f1be22f062 | ||
|
|
cd0afb9c6e | ||
|
|
b2d639265c | ||
|
|
162f630051 | ||
|
|
c877704575 | ||
|
|
8971ec1595 | ||
|
|
af7334dc79 | ||
|
|
0845720a01 | ||
|
|
e5ea72ed2a | ||
|
|
90e2f23f18 | ||
|
|
cf7244a30c | ||
|
|
9f7c107970 | ||
|
|
4528147b26 | ||
|
|
691a8bae55 | ||
|
|
5c5402e61a | ||
|
|
0be540ff83 | ||
|
|
5b31822453 | ||
|
|
c39a59e082 | ||
|
|
9b96a3cad1 | ||
|
|
12c9ad620a | ||
|
|
bcb45c9362 | ||
|
|
30973d1772 | ||
|
|
cd61dc8366 | ||
|
|
53c7f00340 | ||
|
|
732d021b6d | ||
|
|
57b58a2297 | ||
|
|
df8707776c | ||
|
|
88b73a0c3c | ||
|
|
e35fbb26ff | ||
|
|
3f27c2f7c0 | ||
|
|
a88774339c | ||
|
|
0383ef8ec0 | ||
|
|
710b61f887 | ||
|
|
64a9bcefce | ||
|
|
6fd59b4b44 | ||
|
|
2a29d966f5 | ||
|
|
bcef17d731 | ||
|
|
e43bb8df4e | ||
|
|
01f6ea469b | ||
|
|
5f0b2ae798 | ||
|
|
b11de7e294 | ||
|
|
62e8040304 | ||
|
|
61a0cc2480 | ||
|
|
c3bb357513 | ||
|
|
b0c22f6efd | ||
|
|
52a0ef7899 | ||
|
|
494454ce1d | ||
|
|
2f6ee8e698 | ||
|
|
d8cd953f87 | ||
|
|
207c2a7e6b | ||
|
|
4b2c6a2730 | ||
|
|
6b5a20e857 | ||
|
|
8ec45c85ae | ||
|
|
d6633a773a | ||
|
|
ef943381bf | ||
|
|
ed3ae025c2 | ||
|
|
c116551090 | ||
|
|
ec2bf35fe0 | ||
|
|
b5329eab3f | ||
|
|
5314777c46 | ||
|
|
50e5139ff4 | ||
|
|
2d7fc3a83a | ||
|
|
b292a98136 | ||
|
|
5435f805c4 | ||
|
|
273fecaf8b | ||
|
|
8309d457b5 | ||
|
|
857e4af147 | ||
|
|
d84f6745a3 | ||
|
|
cdef14659e | ||
|
|
cb0bc1a545 | ||
|
|
8f6911121c | ||
|
|
5283487f7e | ||
|
|
4462646550 | ||
|
|
84bc7e6005 | ||
|
|
b4ba431a23 | ||
|
|
82513645e7 | ||
|
|
3577ddd630 | ||
|
|
8c4b6a2786 | ||
|
|
2f739d0d55 | ||
|
|
57a2012a85 | ||
|
|
e8514929f5 | ||
|
|
4769953422 | ||
|
|
308dc38875 | ||
|
|
a6d5abf229 | ||
|
|
7ca820295f | ||
|
|
adc587292f | ||
|
|
402e264290 | ||
|
|
607593ff3f | ||
|
|
d2c3ce678a | ||
|
|
e86b7869b9 | ||
|
|
2edce0d282 | ||
|
|
5e06f53d2d | ||
|
|
ce942e8999 | ||
|
|
40e3f93f5c | ||
|
|
39061c244d | ||
|
|
160716d333 | ||
|
|
85bb8b090a | ||
|
|
d0b8679d13 | ||
|
|
72a5c096a2 | ||
|
|
20fd449c39 | ||
|
|
2b3cb3bb92 | ||
|
|
51cc12b229 | ||
|
|
56e4ce937a | ||
|
|
3ce980a5f7 | ||
|
|
29ca022321 | ||
|
|
b3a2ced834 | ||
| 70ff4ab40e | |||
|
|
23e6b1d5d1 | ||
| 70ef10e201 | |||
| f0192795e9 | |||
|
|
d7b8e866b2 | ||
|
|
5793845def | ||
|
|
b01d7a0ed6 | ||
|
|
3084b3d219 | ||
|
|
c187a0fca3 | ||
|
|
e228348deb | ||
|
|
52300969c3 | ||
|
|
dc40e3dc6b | ||
|
|
eca26d557f | ||
|
|
5e164b1f26 | ||
|
|
3aa65f2b11 | ||
|
|
0c9d2fd441 | ||
|
|
d9c9739de1 | ||
|
|
11a7f89216 | ||
|
|
f0178d8928 | ||
|
|
292e47a11e | ||
|
|
405fbaf5da | ||
|
|
bcf64dc95c | ||
|
|
f12f0c34c9 | ||
|
|
6d63c37371 | ||
|
|
964f0030dd | ||
|
|
401ac649ae | ||
|
|
25a7a87712 | ||
|
|
b25a9af824 | ||
|
|
59f68e272c | ||
|
|
daee3e3334 | ||
|
|
169d649ee6 | ||
|
|
0689c5418b | ||
|
|
e9325ff74d | ||
|
|
8c24b0ae24 | ||
| 02e8b36a7a | |||
|
|
36fdb75641 | ||
|
|
dc56468f6b | ||
|
|
62cd1c0938 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -165,6 +165,3 @@ package-lock.json
|
||||
|
||||
# 다운로드용 PPTX 파일은 프로젝트 자산으로 추적
|
||||
!public/downloads/*.pptx
|
||||
|
||||
# 건설PMIS KCC 참고 이미지는 프로젝트 자산으로 추적
|
||||
!public/images/juil/pmis-flow/*.png
|
||||
|
||||
71
CLAUDE.md
71
CLAUDE.md
@@ -126,81 +126,44 @@ ### Tailwind 기본 클래스는 그대로 사용
|
||||
|
||||
---
|
||||
|
||||
## Blade + React(JSX) 혼용 규칙 (필수)
|
||||
|
||||
> **경고: Blade 파일에서 React/JSX 코드 수정 시 반드시 확인하세요!**
|
||||
> **정책 문서**: `/home/aweso/sam/docs/dev/standards/blade-react-policy.md`
|
||||
|
||||
### 핵심 원칙
|
||||
|
||||
Blade 엔진은 `@verbatim` 외부의 모든 이중 중괄호를 PHP echo로 변환한다. JSX의 `style=` 이중중괄호 패턴은 Blade와 충돌하여 **500 에러(ParseError)**를 유발한다.
|
||||
|
||||
### 필수 준수 사항
|
||||
|
||||
```
|
||||
❌ @verbatim 없는 파일에서 style={{ key: 'value' }} 사용 금지
|
||||
❌ JS 주석에 이중 중괄호 텍스트 포함 금지
|
||||
✅ 스타일 객체를 JS 변수로 선언 후 단일 중괄호로 참조
|
||||
✅ React/JSX 코드 수정 전 @verbatim 사용 여부 확인
|
||||
```
|
||||
|
||||
### 허용 패턴
|
||||
|
||||
```javascript
|
||||
// 변수로 분리 (Blade 충돌 없음)
|
||||
const tableStyle = { tableLayout: 'fixed' };
|
||||
const colWidths = [{ width: '10%' }, { width: '22%' }];
|
||||
|
||||
// 단일 중괄호로 참조
|
||||
<table style={tableStyle}>
|
||||
<col style={colWidths[0]} />
|
||||
</table>
|
||||
```
|
||||
|
||||
### 현재 파일 현황
|
||||
|
||||
| 파일 | @verbatim | 주의 |
|
||||
|------|:---------:|:----:|
|
||||
| `finance/journal-entries.blade.php` | ✅ | 안전 |
|
||||
| `barobill/ecard/index.blade.php` | ❌ | 주의 필요 |
|
||||
|
||||
---
|
||||
|
||||
## 데이터베이스 아키텍처 (필수 규칙)
|
||||
|
||||
> **codebridge 서버 분리 이후, MNG는 자체 DB 마이그레이션을 관리합니다.**
|
||||
> **경고: MNG 프로젝트에서는 마이그레이션 파일을 생성하지 않습니다!**
|
||||
|
||||
### 핵심 원칙
|
||||
|
||||
| 작업 | MNG 전용 테이블 | 공용 테이블 |
|
||||
|------|----------------|------------|
|
||||
| 마이그레이션 생성 | ✅ `mng/database/migrations/` | ⚠️ `api/database/migrations/` |
|
||||
| 마이그레이션 실행 | `docker exec sam-mng-1 php artisan migrate` | `docker exec sam-api-1 php artisan migrate` |
|
||||
| 모델 생성 | ✅ `mng/app/Models/` | ✅ `mng/app/Models/` |
|
||||
| 작업 | 올바른 위치 | MNG에서 |
|
||||
|------|------------|---------|
|
||||
| 마이그레이션 생성 | `/home/aweso/sam/api/database/migrations/` | ❌ 금지 |
|
||||
| 마이그레이션 실행 | `docker exec sam-api-1 php artisan migrate` | ❌ 금지 |
|
||||
| 테이블 생성/수정 | API 프로젝트에서만 | ❌ 금지 |
|
||||
|
||||
### MNG database 폴더 상태
|
||||
|
||||
```
|
||||
/home/aweso/sam/mng/database/
|
||||
├── migrations/ ← MNG 전용 테이블 마이그레이션 (예: pmis_*)
|
||||
├── seeders/ ← MNG 전용 시더 (예: MngMenuSeeder)
|
||||
└── factories/ ← 필요 시 사용
|
||||
├── migrations/ ← 비어있음 (파일 생성 금지!)
|
||||
├── seeders/ ← MNG 전용 시더만 허용 (예: MngMenuSeeder)
|
||||
└── factories/ ← 사용 안 함
|
||||
```
|
||||
|
||||
### MNG에서 허용되는 것
|
||||
|
||||
- ✅ 컨트롤러, 뷰, 라우트 작성
|
||||
- ✅ 모델 작성
|
||||
- ✅ MNG 전용 테이블 마이그레이션 생성/실행
|
||||
- ✅ 모델 작성 (API의 테이블 사용)
|
||||
- ✅ MNG 전용 시더 (MngMenuSeeder 등)
|
||||
|
||||
### 공용 테이블이 필요할 때
|
||||
### MNG에서 금지되는 것
|
||||
|
||||
공용 테이블(API와 MNG 모두 사용)은 API에서 관리:
|
||||
- ❌ `database/migrations/` 에 파일 생성
|
||||
- ❌ `docker exec sam-mng-1 php artisan migrate` 실행
|
||||
- ❌ 테이블 구조 변경 관련 작업
|
||||
|
||||
### 새 테이블이 필요할 때
|
||||
|
||||
1. API 프로젝트에서 마이그레이션 생성
|
||||
2. `docker exec sam-api-1 php artisan migrate` 실행
|
||||
3. MNG에서 해당 테이블의 모델 작성
|
||||
3. MNG에서 해당 테이블의 모델만 작성
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -12,4 +12,4 @@ ## 최근 커밋 이력 (참고용)
|
||||
|
||||
## 다음 단계 (필요 시)
|
||||
- API Explorer Phase 2-5 (API 실행, 즐겨찾기, 히스토리, UX 개선)
|
||||
- MNG 견적수식 관리 UI 개발 (`docs/dev/dev_plans/mng-quote-formula-development-plan.md`)
|
||||
- MNG 견적수식 관리 UI 개발 (`docs/plans/mng-quote-formula-development-plan.md`)
|
||||
|
||||
2
Jenkinsfile
vendored
2
Jenkinsfile
vendored
@@ -32,7 +32,6 @@ pipeline {
|
||||
--exclude='.git' \
|
||||
--exclude='.env' \
|
||||
--exclude='storage/app' \
|
||||
--exclude='storage/fonts' \
|
||||
--exclude='storage/logs' \
|
||||
--exclude='storage/framework/sessions' \
|
||||
--exclude='storage/framework/cache' \
|
||||
@@ -47,7 +46,6 @@ pipeline {
|
||||
sudo chmod 640 /home/webservice/mng/shared/.env &&
|
||||
ln -sfn /home/webservice/mng/shared/storage/app storage/app &&
|
||||
ln -sfn /home/webservice/mng/shared/storage/credentials storage/credentials &&
|
||||
rm -rf storage/fonts && ln -sfn /home/webservice/mng/shared/storage/fonts storage/fonts &&
|
||||
composer install --no-dev --optimize-autoloader --no-interaction &&
|
||||
npm install --prefer-offline &&
|
||||
npm run build &&
|
||||
|
||||
@@ -178,54 +178,6 @@ public function restore(Request $request, int $id): JsonResponse
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 일괄 삭제 (Soft Delete)
|
||||
*/
|
||||
public function bulkDelete(Request $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validate(['ids' => 'required|array', 'ids.*' => 'integer']);
|
||||
$deleted = 0;
|
||||
foreach ($validated['ids'] as $id) {
|
||||
if ($this->departmentService->deleteDepartment($id)) {
|
||||
$deleted++;
|
||||
}
|
||||
}
|
||||
|
||||
return response()->json(['success' => true, 'message' => "{$deleted}개 부서가 삭제되었습니다.", 'deleted' => $deleted]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 일괄 복원
|
||||
*/
|
||||
public function bulkRestore(Request $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validate(['ids' => 'required|array', 'ids.*' => 'integer']);
|
||||
$restored = 0;
|
||||
foreach ($validated['ids'] as $id) {
|
||||
if ($this->departmentService->restoreDepartment($id)) {
|
||||
$restored++;
|
||||
}
|
||||
}
|
||||
|
||||
return response()->json(['success' => true, 'message' => "{$restored}개 부서가 복원되었습니다.", 'restored' => $restored]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 일괄 영구삭제 (슈퍼관리자 전용)
|
||||
*/
|
||||
public function bulkForceDelete(Request $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validate(['ids' => 'required|array', 'ids.*' => 'integer']);
|
||||
$deleted = 0;
|
||||
foreach ($validated['ids'] as $id) {
|
||||
if ($this->departmentService->forceDeleteDepartment($id)) {
|
||||
$deleted++;
|
||||
}
|
||||
}
|
||||
|
||||
return response()->json(['success' => true, 'message' => "{$deleted}개 부서가 영구 삭제되었습니다.", 'deleted' => $deleted]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 부서 영구 삭제
|
||||
*/
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
class BusinessIncomePaymentController extends Controller
|
||||
{
|
||||
private const ALLOWED_PAYROLL_USERS = ['이의찬', '전진선', '김보곤'];
|
||||
private const ALLOWED_PAYROLL_USERS = ['이경호', '전진선', '김보곤'];
|
||||
|
||||
public function __construct(
|
||||
private BusinessIncomePaymentService $service
|
||||
@@ -25,10 +25,7 @@ public function __construct(
|
||||
|
||||
private function checkPayrollAccess(): ?JsonResponse
|
||||
{
|
||||
$isAllowedUser = in_array(auth()->user()->name, self::ALLOWED_PAYROLL_USERS);
|
||||
$isDevSuperAdmin = ! app()->environment('production') && auth()->user()->isSuperAdmin();
|
||||
|
||||
if (! $isAllowedUser && ! $isDevSuperAdmin) {
|
||||
if (! in_array(auth()->user()->name, self::ALLOWED_PAYROLL_USERS)) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => '급여관리는 관계자만 볼 수 있습니다.',
|
||||
|
||||
@@ -95,7 +95,6 @@ public function store(Request $request): JsonResponse
|
||||
'address' => 'nullable|string|max:200',
|
||||
'emergency_contact' => 'nullable|string|max:100',
|
||||
'resident_number' => 'nullable|string|max:14',
|
||||
'personal_email' => 'nullable|email|max:100',
|
||||
'bank_account.bank_code' => 'nullable|string|max:20',
|
||||
'bank_account.bank_name' => 'nullable|string|max:50',
|
||||
'bank_account.account_holder' => 'nullable|string|max:50',
|
||||
@@ -177,7 +176,6 @@ public function update(Request $request, int $id): JsonResponse
|
||||
'address' => 'nullable|string|max:200',
|
||||
'emergency_contact' => 'nullable|string|max:100',
|
||||
'resident_number' => 'nullable|string|max:14',
|
||||
'personal_email' => 'nullable|email|max:100',
|
||||
'bank_account.bank_code' => 'nullable|string|max:20',
|
||||
'bank_account.bank_name' => 'nullable|string|max:50',
|
||||
'bank_account.account_holder' => 'nullable|string|max:50',
|
||||
|
||||
@@ -1,139 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\Admin\HR;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\HR\Employee;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class EmployeeSalaryController extends Controller
|
||||
{
|
||||
private const ALLOWED_SALARY_USERS = ['이의찬', '전진선', '김보곤'];
|
||||
|
||||
private function checkSalaryAccess(): ?JsonResponse
|
||||
{
|
||||
$isAllowedUser = in_array(auth()->user()->name, self::ALLOWED_SALARY_USERS);
|
||||
$isDevSuperAdmin = ! app()->environment('production') && auth()->user()->isSuperAdmin();
|
||||
|
||||
if (! $isAllowedUser && ! $isDevSuperAdmin) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => '연봉 정보는 권한이 있는 관계자만 열람할 수 있습니다.',
|
||||
], 403);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 사원 연봉 정보 조회
|
||||
*/
|
||||
public function show(int $id): JsonResponse
|
||||
{
|
||||
if ($denied = $this->checkSalaryAccess()) {
|
||||
return $denied;
|
||||
}
|
||||
|
||||
$employee = Employee::forTenant()->find($id);
|
||||
|
||||
if (! $employee) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => '사원 정보를 찾을 수 없습니다.',
|
||||
], 404);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => $employee->getSalaryInfo(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 사원 연봉 정보 저장/수정
|
||||
*/
|
||||
public function update(Request $request, int $id): JsonResponse
|
||||
{
|
||||
if ($denied = $this->checkSalaryAccess()) {
|
||||
return $denied;
|
||||
}
|
||||
|
||||
$validated = $request->validate([
|
||||
'annual_salary' => 'nullable|integer|min:0',
|
||||
'meal_allowance' => 'nullable|integer|min:0|max:1000000',
|
||||
'fixed_overtime_hours' => 'nullable|integer|min:0|max:52',
|
||||
'effective_date' => 'nullable|date',
|
||||
'notes' => 'nullable|string|max:500',
|
||||
]);
|
||||
|
||||
$employee = Employee::forTenant()->find($id);
|
||||
|
||||
if (! $employee) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => '사원 정보를 찾을 수 없습니다.',
|
||||
], 404);
|
||||
}
|
||||
|
||||
try {
|
||||
$employee->setSalaryInfo($validated);
|
||||
$employee->save();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => '연봉 정보가 저장되었습니다.',
|
||||
'data' => $employee->getSalaryInfo(),
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
report($e);
|
||||
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => '연봉 정보 저장 중 오류가 발생했습니다.',
|
||||
'error' => config('app.debug') ? $e->getMessage() : null,
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 연봉 이력 삭제 (특정 인덱스)
|
||||
*/
|
||||
public function deleteHistory(Request $request, int $id, int $historyIndex): JsonResponse
|
||||
{
|
||||
if ($denied = $this->checkSalaryAccess()) {
|
||||
return $denied;
|
||||
}
|
||||
|
||||
$employee = Employee::forTenant()->find($id);
|
||||
|
||||
if (! $employee) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => '사원 정보를 찾을 수 없습니다.',
|
||||
], 404);
|
||||
}
|
||||
|
||||
$salaryInfo = $employee->getSalaryInfo();
|
||||
$history = $salaryInfo['history'] ?? [];
|
||||
|
||||
if (! isset($history[$historyIndex])) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => '해당 이력을 찾을 수 없습니다.',
|
||||
], 404);
|
||||
}
|
||||
|
||||
array_splice($history, $historyIndex, 1);
|
||||
$salaryInfo['history'] = $history;
|
||||
|
||||
$employee->setJsonExtraValue('salary_info', $salaryInfo);
|
||||
$employee->save();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => '이력이 삭제되었습니다.',
|
||||
'data' => $employee->getSalaryInfo(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -53,8 +53,6 @@ public function store(Request $request): JsonResponse
|
||||
'end_date' => 'required|date|after_or_equal:start_date',
|
||||
'reason' => 'nullable|string|max:1000',
|
||||
'approval_line_id' => 'nullable|integer|exists:approval_lines,id',
|
||||
'references' => 'nullable|array',
|
||||
'references.*.user_id' => 'required|integer|exists:users,id',
|
||||
]);
|
||||
|
||||
try {
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
|
||||
class PayrollController extends Controller
|
||||
{
|
||||
private const ALLOWED_PAYROLL_USERS = ['이의찬', '전진선', '김보곤'];
|
||||
private const ALLOWED_PAYROLL_USERS = ['이경호', '전진선', '김보곤'];
|
||||
|
||||
public function __construct(
|
||||
private PayrollService $payrollService
|
||||
@@ -32,10 +32,7 @@ public function __construct(
|
||||
|
||||
private function checkPayrollAccess(): ?JsonResponse
|
||||
{
|
||||
$isAllowedUser = in_array(auth()->user()->name, self::ALLOWED_PAYROLL_USERS);
|
||||
$isDevSuperAdmin = ! app()->environment('production') && auth()->user()->isSuperAdmin();
|
||||
|
||||
if (! $isAllowedUser && ! $isDevSuperAdmin) {
|
||||
if (! in_array(auth()->user()->name, self::ALLOWED_PAYROLL_USERS)) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => '급여관리는 관계자만 볼 수 있습니다.',
|
||||
@@ -190,8 +187,7 @@ public function update(Request $request, int $id): JsonResponse
|
||||
]);
|
||||
|
||||
try {
|
||||
$isSuperAdmin = auth()->user()?->isSuperAdmin() ?? false;
|
||||
$payroll = $this->payrollService->updatePayroll($id, $validated, $isSuperAdmin);
|
||||
$payroll = $this->payrollService->updatePayroll($id, $validated);
|
||||
|
||||
if (! $payroll) {
|
||||
return response()->json([
|
||||
@@ -364,48 +360,6 @@ public function pay(Request $request, int $id): JsonResponse
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 급여 지급 취소 (paid → draft, 슈퍼관리자 전용)
|
||||
*/
|
||||
public function unpay(Request $request, int $id): JsonResponse
|
||||
{
|
||||
if ($denied = $this->checkPayrollAccess()) {
|
||||
return $denied;
|
||||
}
|
||||
|
||||
if (! auth()->user()?->isSuperAdmin()) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => '슈퍼관리자만 지급 취소할 수 있습니다.',
|
||||
], 403);
|
||||
}
|
||||
|
||||
try {
|
||||
$payroll = $this->payrollService->unpayPayroll($id);
|
||||
|
||||
if (! $payroll) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => '급여 지급을 취소할 수 없습니다.',
|
||||
], 404);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => '급여 지급이 취소되었습니다. (작성중 상태로 변경)',
|
||||
'data' => $payroll,
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
report($e);
|
||||
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => '급여 지급 취소 중 오류가 발생했습니다.',
|
||||
'error' => config('app.debug') ? $e->getMessage() : null,
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 전월 급여 복사 등록
|
||||
*/
|
||||
@@ -784,49 +738,27 @@ public function generateJournalEntry(Request $request): JsonResponse
|
||||
}
|
||||
|
||||
// 해당월 급여 합산
|
||||
$payrolls = Payroll::forTenant($tenantId)
|
||||
$sums = Payroll::forTenant($tenantId)
|
||||
->forPeriod($year, $month)
|
||||
->get();
|
||||
->selectRaw('
|
||||
SUM(gross_salary) as total_gross,
|
||||
SUM(pension) as total_pension,
|
||||
SUM(health_insurance) as total_health,
|
||||
SUM(long_term_care) as total_ltc,
|
||||
SUM(employment_insurance) as total_emp,
|
||||
SUM(income_tax) as total_income_tax,
|
||||
SUM(resident_tax) as total_resident_tax,
|
||||
SUM(net_salary) as total_net
|
||||
')
|
||||
->first();
|
||||
|
||||
if ($payrolls->isEmpty()) {
|
||||
if (! $sums || (int) $sums->total_gross === 0) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => '해당 월 급여 데이터가 없습니다.',
|
||||
], 422);
|
||||
}
|
||||
|
||||
// 기타공제(deductions JSON) 항목별 합산 (이름 정규화)
|
||||
$extraDeductionsByName = [];
|
||||
foreach ($payrolls as $p) {
|
||||
foreach ($p->deductions ?? [] as $d) {
|
||||
$name = trim(preg_replace('/\s+/', ' ', $d['name'] ?? '기타공제'));
|
||||
$amount = (int) ($d['amount'] ?? 0);
|
||||
if ($amount != 0) {
|
||||
$extraDeductionsByName[$name] = ($extraDeductionsByName[$name] ?? 0) + $amount;
|
||||
}
|
||||
}
|
||||
}
|
||||
$extraDeductionsTotal = array_sum($extraDeductionsByName);
|
||||
|
||||
$sums = (object) [
|
||||
'total_gross' => $payrolls->sum('gross_salary'),
|
||||
'total_pension' => $payrolls->sum('pension'),
|
||||
'total_health' => $payrolls->sum('health_insurance'),
|
||||
'total_ltc' => $payrolls->sum('long_term_care'),
|
||||
'total_emp' => $payrolls->sum('employment_insurance'),
|
||||
'total_income_tax' => $payrolls->sum('income_tax'),
|
||||
'total_resident_tax' => $payrolls->sum('resident_tax'),
|
||||
'total_net' => $payrolls->sum('net_salary'),
|
||||
'total_extra_deductions' => $extraDeductionsTotal,
|
||||
];
|
||||
|
||||
if ((int) $sums->total_gross === 0) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => '해당 월 급여 데이터의 총지급액이 0입니다.',
|
||||
], 422);
|
||||
}
|
||||
|
||||
// 거래처 조회
|
||||
$partnerNames = ['임직원', '건강보험연금', '건강보험건강', '건강보험고용', '강서세무서', '강서구청'];
|
||||
$partners = TradingPartner::forTenant($tenantId)
|
||||
@@ -859,33 +791,11 @@ public function generateJournalEntry(Request $request): JsonResponse
|
||||
$monthLabel = "{$month}월분";
|
||||
|
||||
// 분개 행 구성
|
||||
// 양수 공제 → 대변 207 예수금 (원천징수)
|
||||
// 음수 공제 → 차변 207 예수금 (환급, 예수금 감소)
|
||||
// net_salary → DB 실제 값 사용 (역산 아님)
|
||||
$lines = [];
|
||||
$lineNo = 1;
|
||||
|
||||
// 1. 차변: 801 급여 / 임직원
|
||||
$grossAmount = (int) $sums->total_gross;
|
||||
$netSalary = (int) $sums->total_net;
|
||||
|
||||
// 공제항목 정의: [합산값, 거래처, 적요]
|
||||
$deductionItems = [
|
||||
[(int) $sums->total_pension, '건강보험연금', '국민연금'],
|
||||
[(int) $sums->total_health, '건강보험건강', '건강보험'],
|
||||
[(int) $sums->total_ltc, '건강보험건강', '장기요양보험'],
|
||||
[(int) $sums->total_emp, '건강보험고용', '고용보험'],
|
||||
[(int) $sums->total_income_tax, '강서세무서', "{$monthLabel} 근로소득세"],
|
||||
[(int) $sums->total_resident_tax, '강서구청', "{$monthLabel} 지방소득세"],
|
||||
];
|
||||
|
||||
// 기타공제 (항목별)
|
||||
foreach ($extraDeductionsByName as $deductionName => $deductionAmount) {
|
||||
if ($deductionAmount != 0) {
|
||||
$deductionItems[] = [$deductionAmount, '임직원', $deductionName];
|
||||
}
|
||||
}
|
||||
|
||||
// 1. 차변: 801 급여 / 임직원 — 총지급액
|
||||
if ($grossAmount > 0) {
|
||||
$lines[] = [
|
||||
'dc_type' => 'debit',
|
||||
@@ -900,42 +810,104 @@ public function generateJournalEntry(Request $request): JsonResponse
|
||||
];
|
||||
}
|
||||
|
||||
// 2~N. 공제항목: 모두 대변 처리 (음수 공제는 마이너스 대변)
|
||||
// 동일 description + partner 병합을 위해 key 기반 합산
|
||||
$creditLines = [];
|
||||
foreach ($deductionItems as [$amount, $partnerName, $desc]) {
|
||||
if ($amount == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$description = $amount < 0 ? "{$desc} (환급)" : $desc;
|
||||
$mergeKey = "{$partnerName}|{$description}";
|
||||
|
||||
if (isset($creditLines[$mergeKey])) {
|
||||
$creditLines[$mergeKey]['credit_amount'] += $amount;
|
||||
} else {
|
||||
$creditLines[$mergeKey] = [
|
||||
'dc_type' => 'credit',
|
||||
'account_code' => '207',
|
||||
'account_name' => $accountCodes['207'],
|
||||
'trading_partner_id' => $partners[$partnerName],
|
||||
'trading_partner_name' => $partnerName,
|
||||
'debit_amount' => 0,
|
||||
'credit_amount' => $amount,
|
||||
'description' => $description,
|
||||
];
|
||||
}
|
||||
// 2. 대변: 207 예수금 / 건강보험연금 — 국민연금
|
||||
$pension = (int) $sums->total_pension;
|
||||
if ($pension > 0) {
|
||||
$lines[] = [
|
||||
'dc_type' => 'credit',
|
||||
'account_code' => '207',
|
||||
'account_name' => $accountCodes['207'],
|
||||
'trading_partner_id' => $partners['건강보험연금'],
|
||||
'trading_partner_name' => '건강보험연금',
|
||||
'debit_amount' => 0,
|
||||
'credit_amount' => $pension,
|
||||
'description' => '국민연금',
|
||||
'line_no' => $lineNo++,
|
||||
];
|
||||
}
|
||||
|
||||
foreach ($creditLines as $creditLine) {
|
||||
if ($creditLine['credit_amount'] == 0) {
|
||||
continue;
|
||||
}
|
||||
$creditLine['line_no'] = $lineNo++;
|
||||
$lines[] = $creditLine;
|
||||
// 3. 대변: 207 예수금 / 건강보험건강 — 건강보험
|
||||
$health = (int) $sums->total_health;
|
||||
if ($health > 0) {
|
||||
$lines[] = [
|
||||
'dc_type' => 'credit',
|
||||
'account_code' => '207',
|
||||
'account_name' => $accountCodes['207'],
|
||||
'trading_partner_id' => $partners['건강보험건강'],
|
||||
'trading_partner_name' => '건강보험건강',
|
||||
'debit_amount' => 0,
|
||||
'credit_amount' => $health,
|
||||
'description' => '건강보험',
|
||||
'line_no' => $lineNo++,
|
||||
];
|
||||
}
|
||||
|
||||
// 최종: 대변 205 미지급비용 / 임직원 — 실수령액 (DB 값)
|
||||
// 4. 대변: 207 예수금 / 건강보험건강 — 장기요양보험
|
||||
$ltc = (int) $sums->total_ltc;
|
||||
if ($ltc > 0) {
|
||||
$lines[] = [
|
||||
'dc_type' => 'credit',
|
||||
'account_code' => '207',
|
||||
'account_name' => $accountCodes['207'],
|
||||
'trading_partner_id' => $partners['건강보험건강'],
|
||||
'trading_partner_name' => '건강보험건강',
|
||||
'debit_amount' => 0,
|
||||
'credit_amount' => $ltc,
|
||||
'description' => '장기요양보험',
|
||||
'line_no' => $lineNo++,
|
||||
];
|
||||
}
|
||||
|
||||
// 5. 대변: 207 예수금 / 건강보험고용 — 고용보험
|
||||
$emp = (int) $sums->total_emp;
|
||||
if ($emp > 0) {
|
||||
$lines[] = [
|
||||
'dc_type' => 'credit',
|
||||
'account_code' => '207',
|
||||
'account_name' => $accountCodes['207'],
|
||||
'trading_partner_id' => $partners['건강보험고용'],
|
||||
'trading_partner_name' => '건강보험고용',
|
||||
'debit_amount' => 0,
|
||||
'credit_amount' => $emp,
|
||||
'description' => '고용보험',
|
||||
'line_no' => $lineNo++,
|
||||
];
|
||||
}
|
||||
|
||||
// 6. 대변: 207 예수금 / 강서세무서 — 근로소득세
|
||||
$incomeTax = (int) $sums->total_income_tax;
|
||||
if ($incomeTax > 0) {
|
||||
$lines[] = [
|
||||
'dc_type' => 'credit',
|
||||
'account_code' => '207',
|
||||
'account_name' => $accountCodes['207'],
|
||||
'trading_partner_id' => $partners['강서세무서'],
|
||||
'trading_partner_name' => '강서세무서',
|
||||
'debit_amount' => 0,
|
||||
'credit_amount' => $incomeTax,
|
||||
'description' => "{$monthLabel} 근로소득세",
|
||||
'line_no' => $lineNo++,
|
||||
];
|
||||
}
|
||||
|
||||
// 7. 대변: 207 예수금 / 강서구청 — 지방소득세
|
||||
$residentTax = (int) $sums->total_resident_tax;
|
||||
if ($residentTax > 0) {
|
||||
$lines[] = [
|
||||
'dc_type' => 'credit',
|
||||
'account_code' => '207',
|
||||
'account_name' => $accountCodes['207'],
|
||||
'trading_partner_id' => $partners['강서구청'],
|
||||
'trading_partner_name' => '강서구청',
|
||||
'debit_amount' => 0,
|
||||
'credit_amount' => $residentTax,
|
||||
'description' => "{$monthLabel} 지방소득세",
|
||||
'line_no' => $lineNo++,
|
||||
];
|
||||
}
|
||||
|
||||
// 8. 대변: 205 미지급비용 / 임직원 — 급여
|
||||
$netSalary = (int) $sums->total_net;
|
||||
if ($netSalary > 0) {
|
||||
$lines[] = [
|
||||
'dc_type' => 'credit',
|
||||
@@ -955,18 +927,9 @@ public function generateJournalEntry(Request $request): JsonResponse
|
||||
$totalCredit = collect($lines)->sum('credit_amount');
|
||||
|
||||
if ($totalDebit !== $totalCredit || $totalDebit === 0) {
|
||||
$detail = collect($lines)->map(fn ($l) => sprintf(
|
||||
'[%s] %s %s: %s',
|
||||
$l['dc_type'],
|
||||
$l['account_code'],
|
||||
$l['description'],
|
||||
number_format($l['dc_type'] === 'debit' ? $l['debit_amount'] : $l['credit_amount'])
|
||||
))->implode("\n");
|
||||
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => '차변('.number_format($totalDebit).')과 대변('.number_format($totalCredit).")이 일치하지 않습니다.\n차이: ".number_format(abs($totalDebit - $totalCredit)),
|
||||
'detail' => "급여 {$payrolls->count()}건 합산\n\n{$detail}",
|
||||
'message' => "차변({$totalDebit})과 대변({$totalCredit})이 일치하지 않습니다.",
|
||||
], 422);
|
||||
}
|
||||
|
||||
@@ -1017,46 +980,6 @@ public function generateJournalEntry(Request $request): JsonResponse
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 급여명세서 이메일 발송
|
||||
*/
|
||||
public function sendPayslip(Request $request, int $id): JsonResponse
|
||||
{
|
||||
if ($denied = $this->checkPayrollAccess()) {
|
||||
return $denied;
|
||||
}
|
||||
|
||||
try {
|
||||
$result = $this->payrollService->sendPayslip($id);
|
||||
|
||||
if (! $result) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => '급여 정보를 찾을 수 없습니다.',
|
||||
], 404);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => $result['message'],
|
||||
'data' => ['email' => $result['email']],
|
||||
]);
|
||||
} catch (\RuntimeException $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => $e->getMessage(),
|
||||
], 422);
|
||||
} catch (\Throwable $e) {
|
||||
report($e);
|
||||
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => '이메일 발송 중 오류가 발생했습니다.',
|
||||
'error' => config('app.debug') ? $e->getMessage() : null,
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 급여 계산 미리보기 (AJAX)
|
||||
*/
|
||||
|
||||
@@ -115,25 +115,6 @@ public function update(UpdateRoleRequest $request, int $id): JsonResponse
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 일괄 삭제
|
||||
*/
|
||||
public function bulkDelete(Request $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validate(['ids' => 'required|array', 'ids.*' => 'integer']);
|
||||
$deleted = 0;
|
||||
foreach ($validated['ids'] as $id) {
|
||||
try {
|
||||
$this->roleService->deleteRole($id);
|
||||
$deleted++;
|
||||
} catch (\Exception $e) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return response()->json(['success' => true, 'message' => "{$deleted}개 역할이 삭제되었습니다.", 'deleted' => $deleted]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 역할 삭제 (Soft Delete)
|
||||
*/
|
||||
|
||||
@@ -84,14 +84,10 @@ public function list(): JsonResponse
|
||||
->whereRaw('pos_title.`key` COLLATE utf8mb4_unicode_ci = tp.job_title_key COLLATE utf8mb4_unicode_ci');
|
||||
})
|
||||
->whereNull('users.deleted_at')
|
||||
->whereNotNull('tp.department_id')
|
||||
->where(function ($q) {
|
||||
$q->whereNull('tp.employee_status')
|
||||
->orWhere('tp.employee_status', '!=', 'resigned');
|
||||
})
|
||||
->when($tenantId == 1, function ($q) {
|
||||
$q->where('departments.name', '!=', '영업팀');
|
||||
})
|
||||
->orderBy('departments.name')
|
||||
->orderByRaw('COALESCE(pos_rank.sort_order, pos_title.sort_order, 9999) ASC')
|
||||
->orderBy('users.name')
|
||||
|
||||
@@ -294,57 +294,6 @@ public function forceDestroy(Request $request, int $id): JsonResponse
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 일괄 삭제 (Soft Delete)
|
||||
*/
|
||||
public function bulkDelete(Request $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validate(['ids' => 'required|array', 'ids.*' => 'integer']);
|
||||
$deleted = 0;
|
||||
foreach ($validated['ids'] as $id) {
|
||||
if ($this->userService->deleteUser($id)) {
|
||||
$deleted++;
|
||||
}
|
||||
}
|
||||
|
||||
return response()->json(['success' => true, 'message' => "{$deleted}명의 사용자가 삭제되었습니다.", 'deleted' => $deleted]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 일괄 복원
|
||||
*/
|
||||
public function bulkRestore(Request $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validate(['ids' => 'required|array', 'ids.*' => 'integer']);
|
||||
$restored = 0;
|
||||
foreach ($validated['ids'] as $id) {
|
||||
if ($this->userService->restoreUser($id)) {
|
||||
$restored++;
|
||||
}
|
||||
}
|
||||
|
||||
return response()->json(['success' => true, 'message' => "{$restored}명의 사용자가 복원되었습니다.", 'restored' => $restored]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 일괄 영구삭제 (슈퍼관리자 전용)
|
||||
*/
|
||||
public function bulkForceDelete(Request $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validate(['ids' => 'required|array', 'ids.*' => 'integer']);
|
||||
$deleted = 0;
|
||||
foreach ($validated['ids'] as $id) {
|
||||
try {
|
||||
$this->userService->forceDeleteUser($id);
|
||||
$deleted++;
|
||||
} catch (\Exception $e) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return response()->json(['success' => true, 'message' => "{$deleted}명의 사용자가 영구 삭제되었습니다.", 'deleted' => $deleted]);
|
||||
}
|
||||
|
||||
/**
|
||||
* DEV 사이트 자동 로그인 토큰 생성
|
||||
* MNG → DEV 자동 로그인용 One-Time Token 발급
|
||||
|
||||
@@ -1344,15 +1344,11 @@ public function save(Request $request): JsonResponse
|
||||
]);
|
||||
$updated++;
|
||||
} else {
|
||||
$inserted = DB::table('barobill_bank_transactions')->insertOrIgnore(array_merge($data, [
|
||||
DB::table('barobill_bank_transactions')->insert(array_merge($data, [
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]));
|
||||
if ($inserted) {
|
||||
$saved++;
|
||||
} else {
|
||||
$updated++;
|
||||
}
|
||||
$saved++;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\ChinaTech;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\View\View;
|
||||
|
||||
/**
|
||||
* 중국의 기술도약 > 중국 AI기술 컨트롤러
|
||||
*/
|
||||
class ChinaAiController extends Controller
|
||||
{
|
||||
/**
|
||||
* 중국 AI기술 발전과정 페이지
|
||||
*/
|
||||
public function index(Request $request): View|Response
|
||||
{
|
||||
if ($request->header('HX-Request')) {
|
||||
return response('', 200)->header('HX-Redirect', route('china-tech.ai.index'));
|
||||
}
|
||||
|
||||
return view('china-tech.ai.index');
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\ClaudeCode;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\View\View;
|
||||
|
||||
/**
|
||||
* Claude Code > 발전과정 컨트롤러
|
||||
*/
|
||||
class HistoryController extends Controller
|
||||
{
|
||||
public function index(Request $request): View|Response
|
||||
{
|
||||
if ($request->header('HX-Request')) {
|
||||
return response('', 200)->header('HX-Redirect', route('claude-code.history.index'));
|
||||
}
|
||||
|
||||
return view('claude-code.history.index');
|
||||
}
|
||||
}
|
||||
@@ -75,78 +75,6 @@ public function searchPartners(Request $request): JsonResponse
|
||||
return response()->json(['success' => true, 'data' => $data]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 사원 검색 (근로계약서용)
|
||||
*/
|
||||
public function searchEmployees(Request $request): JsonResponse
|
||||
{
|
||||
$q = trim($request->input('q', ''));
|
||||
$tenantId = session('selected_tenant_id', 1);
|
||||
|
||||
$query = \App\Models\HR\Employee::where('tenant_id', $tenantId)
|
||||
->where('employee_status', 'active')
|
||||
->with(['user', 'department']);
|
||||
|
||||
if ($q !== '') {
|
||||
$query->where(function ($w) use ($q) {
|
||||
$w->whereHas('user', fn ($u) => $u->where('name', 'like', "%{$q}%")
|
||||
->orWhere('phone', 'like', "%{$q}%")
|
||||
->orWhere('email', 'like', "%{$q}%"))
|
||||
->orWhereHas('department', fn ($d) => $d->where('name', 'like', "%{$q}%"));
|
||||
});
|
||||
}
|
||||
|
||||
$employees = $query->limit(20)->get();
|
||||
|
||||
$data = $employees->map(function ($emp) {
|
||||
$rn = $emp->resident_number;
|
||||
$birthYear = null;
|
||||
$birthMonth = null;
|
||||
$birthDay = null;
|
||||
|
||||
if ($rn && strlen($rn) >= 7) {
|
||||
$genderDigit = substr($rn, 6, 1);
|
||||
$prefix = in_array($genderDigit, ['3', '4', '7', '8']) ? '20' : '19';
|
||||
$birthYear = $prefix.substr($rn, 0, 2);
|
||||
$birthMonth = substr($rn, 2, 2);
|
||||
$birthDay = substr($rn, 4, 2);
|
||||
}
|
||||
|
||||
$salaryInfo = $emp->getSalaryInfo();
|
||||
|
||||
// 최신 연봉 정보 결정: 현재값 우선, 없으면 이력에서 최신 탐색
|
||||
$annualSalary = $salaryInfo['annual_salary'] ?? null;
|
||||
$salaryEffectiveDate = $salaryInfo['effective_date'] ?? null;
|
||||
|
||||
if ($annualSalary === null && ! empty($salaryInfo['history'])) {
|
||||
$latest = collect($salaryInfo['history'])
|
||||
->sortByDesc('effective_date')
|
||||
->first();
|
||||
$annualSalary = $latest['annual_salary'] ?? null;
|
||||
$salaryEffectiveDate = $latest['effective_date'] ?? null;
|
||||
}
|
||||
|
||||
return [
|
||||
'id' => $emp->id,
|
||||
'name' => $emp->user?->name,
|
||||
'phone' => $emp->user?->phone,
|
||||
'email' => $emp->user?->email,
|
||||
'department' => $emp->department?->name,
|
||||
'position' => $emp->position_label,
|
||||
'job_title' => $emp->job_title_label,
|
||||
'address' => $emp->address,
|
||||
'hire_date' => $emp->hire_date,
|
||||
'birth_year' => $birthYear,
|
||||
'birth_month' => $birthMonth,
|
||||
'birth_day' => $birthDay,
|
||||
'annual_salary' => $annualSalary,
|
||||
'salary_effective_date' => $salaryEffectiveDate,
|
||||
];
|
||||
});
|
||||
|
||||
return response()->json(['success' => true, 'data' => $data]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 고객(명함 등록 고객) 검색
|
||||
*/
|
||||
@@ -644,110 +572,6 @@ public function store(Request $request): JsonResponse
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 계약 수정 (draft 상태만)
|
||||
*/
|
||||
public function update(Request $request, int $id): JsonResponse
|
||||
{
|
||||
$tenantId = session('selected_tenant_id', 1);
|
||||
$contract = EsignContract::forTenant($tenantId)->findOrFail($id);
|
||||
|
||||
if ($contract->status !== 'draft') {
|
||||
return response()->json(['success' => false, 'message' => '초안 상태의 계약만 수정할 수 있습니다.'], 422);
|
||||
}
|
||||
|
||||
$request->validate([
|
||||
'title' => 'required|string|max:200',
|
||||
'description' => 'nullable|string',
|
||||
'sign_order_type' => 'required|in:counterpart_first,creator_first',
|
||||
'expires_at' => 'nullable|date',
|
||||
'signers' => 'required|array|size:2',
|
||||
'signers.*.name' => 'required|string|max:100',
|
||||
'signers.*.email' => 'required|email|max:200',
|
||||
'signers.*.phone' => 'nullable|string|max:20',
|
||||
'signers.*.role' => 'required|in:creator,counterpart',
|
||||
'file' => 'nullable|file|mimes:pdf,doc,docx|max:20480',
|
||||
'fields' => 'nullable|array',
|
||||
'fields.*.id' => 'required|integer',
|
||||
'fields.*.field_value' => 'nullable|string|max:500',
|
||||
]);
|
||||
|
||||
$userId = auth()->id();
|
||||
|
||||
// PDF 파일 교체
|
||||
if ($request->hasFile('file')) {
|
||||
// 기존 파일 삭제
|
||||
if ($contract->original_file_path && Storage::disk('local')->exists($contract->original_file_path)) {
|
||||
Storage::disk('local')->delete($contract->original_file_path);
|
||||
}
|
||||
|
||||
$file = $request->file('file');
|
||||
$converter = new DocxToPdfConverter;
|
||||
$result = $converter->convertAndStore($file, "esign/{$tenantId}/contracts");
|
||||
|
||||
$contract->original_file_path = $result['path'];
|
||||
$contract->original_file_name = $result['name'];
|
||||
$contract->original_file_hash = $result['hash'];
|
||||
$contract->original_file_size = $result['size'];
|
||||
}
|
||||
|
||||
$contract->title = $request->input('title');
|
||||
$contract->description = $request->input('description');
|
||||
$contract->sign_order_type = $request->input('sign_order_type');
|
||||
$contract->expires_at = $request->input('expires_at')
|
||||
? \Carbon\Carbon::parse($request->input('expires_at'))
|
||||
: $contract->expires_at;
|
||||
$contract->updated_by = $userId;
|
||||
$contract->save();
|
||||
|
||||
// 서명자 정보 업데이트
|
||||
$signers = $request->input('signers');
|
||||
foreach ($signers as $signerData) {
|
||||
$existingSigner = EsignSigner::withoutGlobalScopes()
|
||||
->where('contract_id', $contract->id)
|
||||
->where('role', $signerData['role'])
|
||||
->first();
|
||||
|
||||
if ($existingSigner) {
|
||||
$existingSigner->update([
|
||||
'name' => $signerData['name'],
|
||||
'email' => $signerData['email'],
|
||||
'phone' => $signerData['phone'] ?? null,
|
||||
'sign_order' => $signerData['role'] === 'creator'
|
||||
? ($request->input('sign_order_type') === 'creator_first' ? 1 : 2)
|
||||
: ($request->input('sign_order_type') === 'counterpart_first' ? 1 : 2),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// 필드 값 업데이트
|
||||
if ($request->has('fields')) {
|
||||
foreach ($request->input('fields') as $fieldData) {
|
||||
EsignSignField::withoutGlobalScopes()
|
||||
->where('id', $fieldData['id'])
|
||||
->where('contract_id', $contract->id)
|
||||
->update(['field_value' => $fieldData['field_value'] ?? null]);
|
||||
}
|
||||
}
|
||||
|
||||
// 감사 로그
|
||||
EsignAuditLog::create([
|
||||
'tenant_id' => $tenantId,
|
||||
'contract_id' => $contract->id,
|
||||
'action' => 'contract_updated',
|
||||
'ip_address' => $request->ip(),
|
||||
'user_agent' => $request->userAgent(),
|
||||
'metadata' => ['updated_by' => $userId],
|
||||
'created_at' => now(),
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => '계약이 수정되었습니다.',
|
||||
'data' => $contract->load(['signers', 'signFields']),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 계약 취소
|
||||
*/
|
||||
|
||||
@@ -27,15 +27,6 @@ public function create(Request $request): View|Response
|
||||
return view('esign.create');
|
||||
}
|
||||
|
||||
public function edit(Request $request, int $id): View|Response
|
||||
{
|
||||
if ($request->header('HX-Request')) {
|
||||
return response('', 200)->header('HX-Redirect', route('esign.edit', $id));
|
||||
}
|
||||
|
||||
return view('esign.create', ['contractId' => $id]);
|
||||
}
|
||||
|
||||
public function detail(Request $request, int $id): View|Response
|
||||
{
|
||||
if ($request->header('HX-Request')) {
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
use App\Models\Barobill\BankTransactionOverride;
|
||||
use App\Models\Finance\DailyFundMemo;
|
||||
use App\Models\Finance\DailyFundTransaction;
|
||||
use App\Services\Barobill\BarobillBankSyncService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
@@ -174,13 +173,6 @@ public function periodReport(Request $request): JsonResponse
|
||||
$startDateYmd = str_replace('-', '', $startDate);
|
||||
$endDateYmd = str_replace('-', '', $endDate);
|
||||
|
||||
// 바로빌 데이터 자동 동기화 (캐시가 오래되었으면 API에서 갱신)
|
||||
try {
|
||||
app(BarobillBankSyncService::class)->syncIfNeeded($tenantId, $startDateYmd, $endDateYmd);
|
||||
} catch (\Throwable $e) {
|
||||
\Illuminate\Support\Facades\Log::warning('[DailyFund] 바로빌 동기화 실패 (DB 캐시로 계속): '.$e->getMessage());
|
||||
}
|
||||
|
||||
// 기간 내 거래내역 조회 (중복 제거: balance 포함 고유키로 동일 거래만 제거)
|
||||
$transactions = BarobillBankTransaction::where('tenant_id', $tenantId)
|
||||
->whereBetween('trans_date', [$startDateYmd, $endDateYmd])
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Barobill\AccountCode;
|
||||
use App\Models\Barobill\BankTransaction;
|
||||
use App\Models\Barobill\BankTransactionOverride;
|
||||
use App\Models\Barobill\CardTransaction;
|
||||
use App\Models\Barobill\CardTransactionHide;
|
||||
use App\Models\Finance\JournalEntry;
|
||||
@@ -150,8 +149,8 @@ public function store(Request $request): JsonResponse
|
||||
'lines.*.account_name' => 'required|string|max:100',
|
||||
'lines.*.trading_partner_id' => 'nullable|integer',
|
||||
'lines.*.trading_partner_name' => 'nullable|string|max:100',
|
||||
'lines.*.debit_amount' => 'required|integer',
|
||||
'lines.*.credit_amount' => 'required|integer',
|
||||
'lines.*.debit_amount' => 'required|integer|min:0',
|
||||
'lines.*.credit_amount' => 'required|integer|min:0',
|
||||
'lines.*.description' => 'nullable|string|max:300',
|
||||
]);
|
||||
|
||||
@@ -240,15 +239,9 @@ public function update(Request $request, int $id): JsonResponse
|
||||
$tenantId = session('selected_tenant_id', 1);
|
||||
$entry = JournalEntry::forTenant($tenantId)->findOrFail($id);
|
||||
|
||||
// 출처 연결 전표 수정 제한 (카드/홈택스는 원본에서 수정, 계좌/급여는 허용)
|
||||
$editableTypes = ['manual', 'bank_transaction', 'payroll'];
|
||||
if ($entry->source_type && ! in_array($entry->source_type, $editableTypes)) {
|
||||
$sourceLabels = [
|
||||
'ecard_transaction' => '카드사용내역',
|
||||
'hometax_sales' => '홈택스 매출',
|
||||
'hometax_purchase' => '홈택스 매입',
|
||||
];
|
||||
$sourceLabel = $sourceLabels[$entry->source_type] ?? '홈택스 매출/매입';
|
||||
// 출처 연결 전표 수정 제한 (카드/홈택스는 원본에서 수정, 계좌는 허용)
|
||||
if ($entry->source_type && ! in_array($entry->source_type, ['manual', 'bank_transaction'])) {
|
||||
$sourceLabel = $entry->source_type === 'ecard_transaction' ? '카드사용내역' : '홈택스 매출/매입';
|
||||
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
@@ -266,8 +259,8 @@ public function update(Request $request, int $id): JsonResponse
|
||||
'lines.*.account_name' => 'required|string|max:100',
|
||||
'lines.*.trading_partner_id' => 'nullable|integer',
|
||||
'lines.*.trading_partner_name' => 'nullable|string|max:100',
|
||||
'lines.*.debit_amount' => 'required|integer',
|
||||
'lines.*.credit_amount' => 'required|integer',
|
||||
'lines.*.debit_amount' => 'required|integer|min:0',
|
||||
'lines.*.credit_amount' => 'required|integer|min:0',
|
||||
'lines.*.description' => 'nullable|string|max:300',
|
||||
]);
|
||||
|
||||
@@ -310,20 +303,6 @@ public function update(Request $request, int $id): JsonResponse
|
||||
'description' => $line['description'] ?? null,
|
||||
]);
|
||||
}
|
||||
|
||||
// 은행거래 출처 전표의 적요 수정 시 자금일보에도 반영
|
||||
if ($entry->source_type === 'bank_transaction' && $entry->source_key && $request->description) {
|
||||
$existing = BankTransactionOverride::forTenant($tenantId)
|
||||
->byUniqueKey($entry->source_key)
|
||||
->first();
|
||||
|
||||
BankTransactionOverride::saveOverride(
|
||||
$tenantId,
|
||||
$entry->source_key,
|
||||
$request->description,
|
||||
$existing->modified_cast ?? null
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return response()->json([
|
||||
@@ -398,7 +377,7 @@ public function tradingPartners(Request $request): JsonResponse
|
||||
});
|
||||
}
|
||||
|
||||
$partners = $query->orderBy('name')->get();
|
||||
$partners = $query->orderBy('name')->limit(50)->get();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
@@ -585,8 +564,8 @@ public function storeFromBank(Request $request): JsonResponse
|
||||
'lines.*.account_name' => 'required|string|max:100',
|
||||
'lines.*.trading_partner_id' => 'nullable|integer',
|
||||
'lines.*.trading_partner_name' => 'nullable|string|max:100',
|
||||
'lines.*.debit_amount' => 'required|integer',
|
||||
'lines.*.credit_amount' => 'required|integer',
|
||||
'lines.*.debit_amount' => 'required|integer|min:0',
|
||||
'lines.*.credit_amount' => 'required|integer|min:0',
|
||||
'lines.*.description' => 'nullable|string|max:300',
|
||||
]);
|
||||
|
||||
@@ -1053,8 +1032,8 @@ public function storeFromCard(Request $request): JsonResponse
|
||||
'lines.*.account_name' => 'required|string|max:100',
|
||||
'lines.*.trading_partner_id' => 'nullable|integer',
|
||||
'lines.*.trading_partner_name' => 'nullable|string|max:100',
|
||||
'lines.*.debit_amount' => 'required|integer',
|
||||
'lines.*.credit_amount' => 'required|integer',
|
||||
'lines.*.debit_amount' => 'required|integer|min:0',
|
||||
'lines.*.credit_amount' => 'required|integer|min:0',
|
||||
'lines.*.description' => 'nullable|string|max:300',
|
||||
]);
|
||||
|
||||
|
||||
@@ -83,7 +83,6 @@ public function store(Request $request): JsonResponse
|
||||
{
|
||||
$request->validate([
|
||||
'name' => 'required|string|max:100',
|
||||
'bizNo' => 'required|string|max:20',
|
||||
'type' => 'nullable|string|max:100',
|
||||
'category' => 'nullable|string|max:100',
|
||||
]);
|
||||
@@ -93,9 +92,9 @@ public function store(Request $request): JsonResponse
|
||||
$partner = TradingPartner::create([
|
||||
'tenant_id' => $tenantId,
|
||||
'name' => $request->input('name'),
|
||||
'trade_type' => $request->input('tradeType') ?? 'sales',
|
||||
'type' => $request->input('type') ?? 'vendor',
|
||||
'category' => $request->input('category') ?? '기타',
|
||||
'trade_type' => $request->input('tradeType', 'sales'),
|
||||
'type' => $request->input('type'),
|
||||
'category' => $request->input('category'),
|
||||
'biz_no' => $request->input('bizNo'),
|
||||
'ceo' => $request->input('ceo'),
|
||||
'bank_account' => $request->input('bankAccount'),
|
||||
@@ -104,7 +103,7 @@ public function store(Request $request): JsonResponse
|
||||
'address' => $request->input('address'),
|
||||
'manager' => $request->input('manager'),
|
||||
'manager_phone' => $request->input('managerPhone'),
|
||||
'status' => $request->input('status') ?? 'active',
|
||||
'status' => $request->input('status', 'active'),
|
||||
'memo' => $request->input('memo'),
|
||||
]);
|
||||
|
||||
@@ -145,8 +144,8 @@ public function update(Request $request, int $id): JsonResponse
|
||||
$partner->update([
|
||||
'name' => $request->input('name'),
|
||||
'trade_type' => $request->input('tradeType', $partner->trade_type),
|
||||
'type' => $request->input('type') ?? $partner->type,
|
||||
'category' => $request->input('category') ?? $partner->category,
|
||||
'type' => $request->input('type'),
|
||||
'category' => $request->input('category'),
|
||||
'biz_no' => $request->input('bizNo'),
|
||||
'ceo' => $request->input('ceo'),
|
||||
'bank_account' => $request->input('bankAccount'),
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
class BusinessIncomePaymentController extends Controller
|
||||
{
|
||||
private const ALLOWED_PAYROLL_USERS = ['이의찬', '전진선', '김보곤'];
|
||||
private const ALLOWED_PAYROLL_USERS = ['이경호', '전진선', '김보곤'];
|
||||
|
||||
public function __construct(
|
||||
private BusinessIncomePaymentService $service
|
||||
@@ -25,10 +25,7 @@ public function index(Request $request): View|Response
|
||||
return response('', 200)->header('HX-Redirect', route('hr.business-income-payments.index'));
|
||||
}
|
||||
|
||||
$isAllowedUser = in_array(auth()->user()->name, self::ALLOWED_PAYROLL_USERS);
|
||||
$isDevSuperAdmin = ! app()->environment('production') && auth()->user()->isSuperAdmin();
|
||||
|
||||
if (! $isAllowedUser && ! $isDevSuperAdmin) {
|
||||
if (! in_array(auth()->user()->name, self::ALLOWED_PAYROLL_USERS)) {
|
||||
return view('hr.payrolls.restricted');
|
||||
}
|
||||
|
||||
|
||||
@@ -9,20 +9,10 @@
|
||||
|
||||
class EmployeeController extends Controller
|
||||
{
|
||||
private const ALLOWED_SALARY_USERS = ['이의찬', '전진선', '김보곤'];
|
||||
|
||||
public function __construct(
|
||||
private EmployeeService $employeeService
|
||||
) {}
|
||||
|
||||
private function canViewSalary(): bool
|
||||
{
|
||||
$isAllowedUser = in_array(auth()->user()->name, self::ALLOWED_SALARY_USERS);
|
||||
$isDevSuperAdmin = ! app()->environment('production') && auth()->user()->isSuperAdmin();
|
||||
|
||||
return $isAllowedUser || $isDevSuperAdmin;
|
||||
}
|
||||
|
||||
/**
|
||||
* 사원 목록 페이지
|
||||
*/
|
||||
@@ -72,13 +62,9 @@ public function show(int $id): View
|
||||
->orderBy('created_at', 'desc')
|
||||
->get();
|
||||
|
||||
$canViewSalary = $this->canViewSalary();
|
||||
|
||||
return view('hr.employees.show', [
|
||||
'employee' => $employee,
|
||||
'files' => $files,
|
||||
'canViewSalary' => $canViewSalary,
|
||||
'salaryInfo' => $canViewSalary ? $employee->getSalaryInfo() : null,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -103,8 +89,6 @@ public function edit(int $id): View
|
||||
->orderBy('created_at', 'desc')
|
||||
->get();
|
||||
|
||||
$canViewSalary = $this->canViewSalary();
|
||||
|
||||
return view('hr.employees.edit', [
|
||||
'employee' => $employee,
|
||||
'departments' => $departments,
|
||||
@@ -112,8 +96,6 @@ public function edit(int $id): View
|
||||
'titles' => $titles,
|
||||
'banks' => config('banks', []),
|
||||
'files' => $files,
|
||||
'canViewSalary' => $canViewSalary,
|
||||
'salaryInfo' => $canViewSalary ? $employee->getSalaryInfo() : null,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
class PayrollController extends Controller
|
||||
{
|
||||
private const ALLOWED_PAYROLL_USERS = ['이의찬', '전진선', '김보곤'];
|
||||
private const ALLOWED_PAYROLL_USERS = ['이경호', '전진선', '김보곤'];
|
||||
|
||||
public function __construct(
|
||||
private PayrollService $payrollService
|
||||
@@ -26,10 +26,7 @@ public function index(Request $request): View|Response
|
||||
return response('', 200)->header('HX-Redirect', route('hr.payrolls.index'));
|
||||
}
|
||||
|
||||
$isAllowedUser = in_array(auth()->user()->name, self::ALLOWED_PAYROLL_USERS);
|
||||
$isDevSuperAdmin = ! app()->environment('production') && auth()->user()->isSuperAdmin();
|
||||
|
||||
if (! $isAllowedUser && ! $isDevSuperAdmin) {
|
||||
if (! in_array(auth()->user()->name, self::ALLOWED_PAYROLL_USERS)) {
|
||||
return view('hr.payrolls.restricted');
|
||||
}
|
||||
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Help;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\View\View;
|
||||
|
||||
/**
|
||||
* 도움말 > 회계동작원리 컨트롤러
|
||||
*/
|
||||
class AccountingGuideController extends Controller
|
||||
{
|
||||
public function index(Request $request): View|Response
|
||||
{
|
||||
if ($request->header('HX-Request')) {
|
||||
return response('', 200)->header('HX-Redirect', route('help.accounting.index'));
|
||||
}
|
||||
|
||||
return view('help.accounting.index');
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Help;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\View\View;
|
||||
|
||||
/**
|
||||
* 도움말 > 연차휴가/근태관리 컨트롤러
|
||||
*/
|
||||
class AttendanceGuideController extends Controller
|
||||
{
|
||||
public function index(Request $request): View|Response
|
||||
{
|
||||
if ($request->header('HX-Request')) {
|
||||
return response('', 200)->header('HX-Redirect', route('help.attendance.index'));
|
||||
}
|
||||
|
||||
return view('help.attendance.index');
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Help;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\View\View;
|
||||
|
||||
/**
|
||||
* 도움말 > 바로빌 연동 가이드 컨트롤러
|
||||
*/
|
||||
class BarobillGuideController extends Controller
|
||||
{
|
||||
public function index(Request $request): View|Response
|
||||
{
|
||||
if ($request->header('HX-Request')) {
|
||||
return response('', 200)->header('HX-Redirect', route('help.barobill.index'));
|
||||
}
|
||||
|
||||
return view('help.barobill.index');
|
||||
}
|
||||
}
|
||||
@@ -3,9 +3,6 @@
|
||||
namespace App\Http\Controllers\Juil;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Juil\PmisWorker;
|
||||
use App\Services\WeatherService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\View\View;
|
||||
@@ -38,136 +35,4 @@ public function workflow(Request $request): View|Response
|
||||
|
||||
return view('juil.workflow');
|
||||
}
|
||||
|
||||
public function constructionPmis(Request $request): View|Response
|
||||
{
|
||||
if ($request->header('HX-Request')) {
|
||||
return response('', 200)->header('HX-Redirect', route('juil.construction-pmis'));
|
||||
}
|
||||
|
||||
return view('juil.construction-pmis');
|
||||
}
|
||||
|
||||
public function bimViewer(Request $request): View|Response
|
||||
{
|
||||
if ($request->header('HX-Request')) {
|
||||
return response('', 200)->header('HX-Redirect', route('juil.construction-pmis.bim-viewer'));
|
||||
}
|
||||
|
||||
return view('juil.bim-viewer');
|
||||
}
|
||||
|
||||
// ── 시공관리 ──
|
||||
|
||||
public function pmisWorkforce(Request $request): View|Response
|
||||
{
|
||||
if ($request->header('HX-Request')) {
|
||||
return response('', 200)->header('HX-Redirect', route('juil.construction-pmis.workforce'));
|
||||
}
|
||||
|
||||
return view('juil.pmis-workforce');
|
||||
}
|
||||
|
||||
public function pmisEquipment(Request $request): View|Response
|
||||
{
|
||||
if ($request->header('HX-Request')) {
|
||||
return response('', 200)->header('HX-Redirect', route('juil.construction-pmis.equipment'));
|
||||
}
|
||||
|
||||
return view('juil.pmis-equipment');
|
||||
}
|
||||
|
||||
public function pmisMaterials(Request $request): View|Response
|
||||
{
|
||||
if ($request->header('HX-Request')) {
|
||||
return response('', 200)->header('HX-Redirect', route('juil.construction-pmis.materials'));
|
||||
}
|
||||
|
||||
return view('juil.pmis-materials');
|
||||
}
|
||||
|
||||
public function pmisWorkVolume(Request $request): View|Response
|
||||
{
|
||||
if ($request->header('HX-Request')) {
|
||||
return response('', 200)->header('HX-Redirect', route('juil.construction-pmis.work-volume'));
|
||||
}
|
||||
|
||||
return view('juil.pmis-work-volume');
|
||||
}
|
||||
|
||||
public function pmisDailyAttendance(Request $request): View|Response
|
||||
{
|
||||
if ($request->header('HX-Request')) {
|
||||
return response('', 200)->header('HX-Redirect', route('juil.construction-pmis.daily-attendance'));
|
||||
}
|
||||
|
||||
return view('juil.pmis-daily-attendance');
|
||||
}
|
||||
|
||||
public function pmisDailyReport(Request $request): View|Response
|
||||
{
|
||||
if ($request->header('HX-Request')) {
|
||||
return response('', 200)->header('HX-Redirect', route('juil.construction-pmis.daily-report'));
|
||||
}
|
||||
|
||||
return view('juil.pmis-daily-report');
|
||||
}
|
||||
|
||||
public function pmisWeather(WeatherService $weatherService): JsonResponse
|
||||
{
|
||||
$forecasts = $weatherService->getWeeklyForecast();
|
||||
|
||||
return response()->json(['forecasts' => array_slice($forecasts, 0, 2)]);
|
||||
}
|
||||
|
||||
public function pmisProfile(): JsonResponse
|
||||
{
|
||||
$user = auth()->user();
|
||||
$tenantId = session('current_tenant_id', 1);
|
||||
$worker = PmisWorker::findOrCreateFromUser($user, $tenantId);
|
||||
|
||||
return response()->json([
|
||||
'worker' => [
|
||||
'id' => $worker->id,
|
||||
'name' => $worker->name,
|
||||
'login_id' => $worker->login_id,
|
||||
'phone' => $worker->phone,
|
||||
'email' => $worker->email,
|
||||
'department' => $worker->department ?? '-',
|
||||
'position' => $worker->position ?? '-',
|
||||
'role_type' => $worker->role_type ?? '-',
|
||||
'gender' => $worker->gender ?? '',
|
||||
'company' => $worker->company ?? '-',
|
||||
'profile_photo_path' => $worker->profile_photo_path,
|
||||
'created_at' => $worker->created_at?->format('Y-m-d'),
|
||||
'last_login_at' => $worker->last_login_at?->format('Y-m-d H:i')
|
||||
?? $user->last_login_at?->format('Y-m-d H:i'),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function pmisProfileUpdate(Request $request): JsonResponse
|
||||
{
|
||||
$request->validate([
|
||||
'phone' => ['nullable', 'string', 'max:20'],
|
||||
'email' => ['nullable', 'email', 'max:255'],
|
||||
'gender' => ['nullable', 'string', 'in:남,여'],
|
||||
'position' => ['nullable', 'string', 'max:50'],
|
||||
'company' => ['nullable', 'string', 'max:100'],
|
||||
]);
|
||||
|
||||
$user = auth()->user();
|
||||
$tenantId = session('current_tenant_id', 1);
|
||||
$worker = PmisWorker::findOrCreateFromUser($user, $tenantId);
|
||||
|
||||
$worker->update([
|
||||
'phone' => $request->input('phone'),
|
||||
'email' => $request->input('email'),
|
||||
'gender' => $request->input('gender'),
|
||||
'position' => $request->input('position'),
|
||||
'company' => $request->input('company'),
|
||||
]);
|
||||
|
||||
return response()->json(['success' => true, 'message' => '개인정보가 저장되었습니다.']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,212 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Juil;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Juil\PmisAttendanceEquipment;
|
||||
use App\Models\Juil\PmisAttendanceWorker;
|
||||
use App\Models\Juil\PmisDailyAttendance;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class PmisDailyAttendanceController extends Controller
|
||||
{
|
||||
private function tenantId(): int
|
||||
{
|
||||
return (int) session('current_tenant_id', 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 해당 날짜의 출면일보 조회 (없으면 생성)
|
||||
*/
|
||||
public function show(Request $request): JsonResponse
|
||||
{
|
||||
$date = $request->input('date', now()->toDateString());
|
||||
$company = $request->input('company', '');
|
||||
|
||||
$attendance = PmisDailyAttendance::tenant($this->tenantId())
|
||||
->where('date', $date)
|
||||
->when($company, fn ($q) => $q->where('company_name', $company))
|
||||
->first();
|
||||
|
||||
if (! $attendance) {
|
||||
$attendance = PmisDailyAttendance::create([
|
||||
'tenant_id' => $this->tenantId(),
|
||||
'date' => $date,
|
||||
'company_name' => $company,
|
||||
'weather' => '맑음',
|
||||
'status' => 'draft',
|
||||
]);
|
||||
}
|
||||
|
||||
$attendance->load(['workers', 'equipments']);
|
||||
|
||||
return response()->json($attendance);
|
||||
}
|
||||
|
||||
/**
|
||||
* 해당 월의 일별 상태 조회 (캘린더 닷 표시용)
|
||||
*/
|
||||
public function monthStatus(Request $request): JsonResponse
|
||||
{
|
||||
$year = $request->integer('year', now()->year);
|
||||
$month = $request->integer('month', now()->month);
|
||||
$company = $request->input('company', '');
|
||||
|
||||
$attendances = PmisDailyAttendance::tenant($this->tenantId())
|
||||
->whereYear('date', $year)
|
||||
->whereMonth('date', $month)
|
||||
->when($company, fn ($q) => $q->where('company_name', $company))
|
||||
->withCount(['workers', 'equipments'])
|
||||
->get();
|
||||
|
||||
$result = [];
|
||||
foreach ($attendances as $att) {
|
||||
$day = (int) $att->date->format('d');
|
||||
if ($att->workers_count > 0 || $att->equipments_count > 0) {
|
||||
$result[$day] = $att->status;
|
||||
}
|
||||
}
|
||||
|
||||
return response()->json($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 출면일보 메타 업데이트 (날씨, 특이사항, 상태)
|
||||
*/
|
||||
public function update(Request $request, int $id): JsonResponse
|
||||
{
|
||||
$attendance = PmisDailyAttendance::tenant($this->tenantId())->findOrFail($id);
|
||||
|
||||
$validated = $request->validate([
|
||||
'weather' => 'sometimes|string|max:50',
|
||||
'notes' => 'sometimes|nullable|string',
|
||||
'status' => 'sometimes|in:draft,review,approved',
|
||||
'options' => 'sometimes|nullable|array',
|
||||
]);
|
||||
|
||||
$attendance->update($validated);
|
||||
|
||||
return response()->json($attendance);
|
||||
}
|
||||
|
||||
/**
|
||||
* 검토자 저장
|
||||
*/
|
||||
public function saveReviewers(Request $request, int $id): JsonResponse
|
||||
{
|
||||
$attendance = PmisDailyAttendance::tenant($this->tenantId())->findOrFail($id);
|
||||
|
||||
$reviewers = $request->input('reviewers', []);
|
||||
$options = $attendance->options ?? [];
|
||||
$options['reviewers'] = $reviewers;
|
||||
$attendance->update(['options' => $options]);
|
||||
|
||||
return response()->json(['message' => '검토자가 저장되었습니다.']);
|
||||
}
|
||||
|
||||
// ─── 인원(Worker) CRUD ───
|
||||
|
||||
public function workerStore(Request $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'attendance_id' => 'required|integer|exists:pmis_daily_attendances,id',
|
||||
'work_type' => 'required|string|max:200',
|
||||
'job_type' => 'required|string|max:200',
|
||||
'name' => 'required|string|max:100',
|
||||
'man_days' => 'nullable|numeric|min:0',
|
||||
'amount' => 'nullable|numeric|min:0',
|
||||
'work_content' => 'nullable|string|max:500',
|
||||
]);
|
||||
|
||||
$validated['tenant_id'] = $this->tenantId();
|
||||
$validated['man_days'] = $validated['man_days'] ?? 1.0;
|
||||
$validated['amount'] = $validated['amount'] ?? 0;
|
||||
$validated['work_content'] = $validated['work_content'] ?? '';
|
||||
|
||||
$maxSort = PmisAttendanceWorker::where('attendance_id', $validated['attendance_id'])->max('sort_order') ?? 0;
|
||||
$validated['sort_order'] = $maxSort + 1;
|
||||
|
||||
$worker = PmisAttendanceWorker::create($validated);
|
||||
|
||||
return response()->json($worker, 201);
|
||||
}
|
||||
|
||||
public function workerUpdate(Request $request, int $id): JsonResponse
|
||||
{
|
||||
$worker = PmisAttendanceWorker::where('tenant_id', $this->tenantId())->findOrFail($id);
|
||||
|
||||
$validated = $request->validate([
|
||||
'work_type' => 'sometimes|string|max:200',
|
||||
'job_type' => 'sometimes|string|max:200',
|
||||
'name' => 'sometimes|string|max:100',
|
||||
'man_days' => 'nullable|numeric|min:0',
|
||||
'amount' => 'nullable|numeric|min:0',
|
||||
'work_content' => 'nullable|string|max:500',
|
||||
]);
|
||||
|
||||
$worker->update($validated);
|
||||
|
||||
return response()->json($worker);
|
||||
}
|
||||
|
||||
public function workerDestroy(int $id): JsonResponse
|
||||
{
|
||||
$worker = PmisAttendanceWorker::where('tenant_id', $this->tenantId())->findOrFail($id);
|
||||
$worker->delete();
|
||||
|
||||
return response()->json(['message' => '삭제되었습니다.']);
|
||||
}
|
||||
|
||||
// ─── 장비(Equipment) CRUD ───
|
||||
|
||||
public function equipmentStore(Request $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'attendance_id' => 'required|integer|exists:pmis_daily_attendances,id',
|
||||
'equipment_name' => 'required|string|max:200',
|
||||
'specification' => 'nullable|string|max:300',
|
||||
'equipment_number' => 'nullable|string|max:100',
|
||||
'operator' => 'nullable|string|max:100',
|
||||
'man_days' => 'nullable|numeric|min:0',
|
||||
'work_content' => 'nullable|string|max:500',
|
||||
]);
|
||||
|
||||
$validated['tenant_id'] = $this->tenantId();
|
||||
$validated['man_days'] = $validated['man_days'] ?? 1.0;
|
||||
$validated['work_content'] = $validated['work_content'] ?? '';
|
||||
|
||||
$maxSort = PmisAttendanceEquipment::where('attendance_id', $validated['attendance_id'])->max('sort_order') ?? 0;
|
||||
$validated['sort_order'] = $maxSort + 1;
|
||||
|
||||
$equipment = PmisAttendanceEquipment::create($validated);
|
||||
|
||||
return response()->json($equipment, 201);
|
||||
}
|
||||
|
||||
public function equipmentUpdate(Request $request, int $id): JsonResponse
|
||||
{
|
||||
$equipment = PmisAttendanceEquipment::where('tenant_id', $this->tenantId())->findOrFail($id);
|
||||
|
||||
$validated = $request->validate([
|
||||
'equipment_name' => 'sometimes|string|max:200',
|
||||
'specification' => 'nullable|string|max:300',
|
||||
'equipment_number' => 'nullable|string|max:100',
|
||||
'operator' => 'nullable|string|max:100',
|
||||
'man_days' => 'nullable|numeric|min:0',
|
||||
'work_content' => 'nullable|string|max:500',
|
||||
]);
|
||||
|
||||
$equipment->update($validated);
|
||||
|
||||
return response()->json($equipment);
|
||||
}
|
||||
|
||||
public function equipmentDestroy(int $id): JsonResponse
|
||||
{
|
||||
$equipment = PmisAttendanceEquipment::where('tenant_id', $this->tenantId())->findOrFail($id);
|
||||
$equipment->delete();
|
||||
|
||||
return response()->json(['message' => '삭제되었습니다.']);
|
||||
}
|
||||
}
|
||||
@@ -1,314 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Juil;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Juil\PmisDailyWorkReport;
|
||||
use App\Models\Juil\PmisWorkReportEquipment;
|
||||
use App\Models\Juil\PmisWorkReportMaterial;
|
||||
use App\Models\Juil\PmisWorkReportPhoto;
|
||||
use App\Models\Juil\PmisWorkReportVolume;
|
||||
use App\Models\Juil\PmisWorkReportWorker;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class PmisDailyWorkReportController extends Controller
|
||||
{
|
||||
private function tenantId(): int
|
||||
{
|
||||
return (int) session('current_tenant_id', 1);
|
||||
}
|
||||
|
||||
public function show(Request $request): JsonResponse
|
||||
{
|
||||
$date = $request->input('date', now()->toDateString());
|
||||
$company = $request->input('company', '');
|
||||
|
||||
$report = PmisDailyWorkReport::tenant($this->tenantId())
|
||||
->where('date', $date)
|
||||
->when($company, fn ($q) => $q->where('company_name', $company))
|
||||
->first();
|
||||
|
||||
if (! $report) {
|
||||
$report = PmisDailyWorkReport::create([
|
||||
'tenant_id' => $this->tenantId(),
|
||||
'date' => $date,
|
||||
'company_name' => $company,
|
||||
'weather' => '맑음',
|
||||
'status' => 'draft',
|
||||
]);
|
||||
}
|
||||
|
||||
$report->load(['workers', 'equipments', 'materials', 'volumes', 'photos']);
|
||||
|
||||
return response()->json($report);
|
||||
}
|
||||
|
||||
public function monthStatus(Request $request): JsonResponse
|
||||
{
|
||||
$year = $request->integer('year', now()->year);
|
||||
$month = $request->integer('month', now()->month);
|
||||
$company = $request->input('company', '');
|
||||
|
||||
$reports = PmisDailyWorkReport::tenant($this->tenantId())
|
||||
->whereYear('date', $year)
|
||||
->whereMonth('date', $month)
|
||||
->when($company, fn ($q) => $q->where('company_name', $company))
|
||||
->withCount(['workers', 'equipments', 'materials', 'volumes', 'photos'])
|
||||
->get();
|
||||
|
||||
$result = [];
|
||||
foreach ($reports as $r) {
|
||||
$day = (int) $r->date->format('d');
|
||||
$hasData = $r->workers_count > 0 || $r->equipments_count > 0
|
||||
|| $r->materials_count > 0 || $r->volumes_count > 0
|
||||
|| $r->photos_count > 0
|
||||
|| $r->work_content_today;
|
||||
if ($hasData) {
|
||||
$result[$day] = $r->status;
|
||||
}
|
||||
}
|
||||
|
||||
return response()->json($result);
|
||||
}
|
||||
|
||||
public function update(Request $request, int $id): JsonResponse
|
||||
{
|
||||
$report = PmisDailyWorkReport::tenant($this->tenantId())->findOrFail($id);
|
||||
|
||||
$validated = $request->validate([
|
||||
'weather' => 'sometimes|string|max:50',
|
||||
'temp_low' => 'sometimes|nullable|numeric',
|
||||
'temp_high' => 'sometimes|nullable|numeric',
|
||||
'precipitation' => 'sometimes|nullable|numeric',
|
||||
'snowfall' => 'sometimes|nullable|numeric',
|
||||
'fine_dust' => 'sometimes|nullable|string|max:50',
|
||||
'ultra_fine_dust' => 'sometimes|nullable|string|max:50',
|
||||
'work_content_today' => 'sometimes|nullable|string',
|
||||
'work_content_tomorrow' => 'sometimes|nullable|string',
|
||||
'notes' => 'sometimes|nullable|string',
|
||||
'status' => 'sometimes|in:draft,review,approved',
|
||||
'options' => 'sometimes|nullable|array',
|
||||
]);
|
||||
|
||||
$report->update($validated);
|
||||
|
||||
return response()->json($report);
|
||||
}
|
||||
|
||||
public function destroy(int $id): JsonResponse
|
||||
{
|
||||
$report = PmisDailyWorkReport::tenant($this->tenantId())->findOrFail($id);
|
||||
$report->delete();
|
||||
|
||||
return response()->json(['message' => '삭제되었습니다.']);
|
||||
}
|
||||
|
||||
public function saveReviewers(Request $request, int $id): JsonResponse
|
||||
{
|
||||
$report = PmisDailyWorkReport::tenant($this->tenantId())->findOrFail($id);
|
||||
$reviewers = $request->input('reviewers', []);
|
||||
$options = $report->options ?? [];
|
||||
$options['reviewers'] = $reviewers;
|
||||
$report->update(['options' => $options]);
|
||||
|
||||
return response()->json(['message' => '검토자가 저장되었습니다.']);
|
||||
}
|
||||
|
||||
// ─── Worker CRUD ───
|
||||
|
||||
public function workerStore(Request $request): JsonResponse
|
||||
{
|
||||
$v = $request->validate([
|
||||
'report_id' => 'required|integer|exists:pmis_daily_work_reports,id',
|
||||
'work_type' => 'required|string|max:200',
|
||||
'job_type' => 'required|string|max:200',
|
||||
'prev_cumulative' => 'nullable|integer|min:0',
|
||||
'today_count' => 'nullable|integer|min:0',
|
||||
]);
|
||||
$v['tenant_id'] = $this->tenantId();
|
||||
$v['prev_cumulative'] = $v['prev_cumulative'] ?? 0;
|
||||
$v['today_count'] = $v['today_count'] ?? 0;
|
||||
$v['sort_order'] = (PmisWorkReportWorker::where('report_id', $v['report_id'])->max('sort_order') ?? 0) + 1;
|
||||
|
||||
return response()->json(PmisWorkReportWorker::create($v), 201);
|
||||
}
|
||||
|
||||
public function workerUpdate(Request $request, int $id): JsonResponse
|
||||
{
|
||||
$w = PmisWorkReportWorker::where('tenant_id', $this->tenantId())->findOrFail($id);
|
||||
$w->update($request->validate([
|
||||
'work_type' => 'sometimes|string|max:200',
|
||||
'job_type' => 'sometimes|string|max:200',
|
||||
'prev_cumulative' => 'nullable|integer|min:0',
|
||||
'today_count' => 'nullable|integer|min:0',
|
||||
]));
|
||||
|
||||
return response()->json($w);
|
||||
}
|
||||
|
||||
public function workerDestroy(int $id): JsonResponse
|
||||
{
|
||||
PmisWorkReportWorker::where('tenant_id', $this->tenantId())->findOrFail($id)->delete();
|
||||
|
||||
return response()->json(['message' => '삭제되었습니다.']);
|
||||
}
|
||||
|
||||
// ─── Equipment CRUD ───
|
||||
|
||||
public function equipmentStore(Request $request): JsonResponse
|
||||
{
|
||||
$v = $request->validate([
|
||||
'report_id' => 'required|integer|exists:pmis_daily_work_reports,id',
|
||||
'equipment_name' => 'required|string|max:200',
|
||||
'specification' => 'nullable|string|max:300',
|
||||
'prev_cumulative' => 'nullable|integer|min:0',
|
||||
'today_count' => 'nullable|integer|min:0',
|
||||
]);
|
||||
$v['tenant_id'] = $this->tenantId();
|
||||
$v['specification'] = $v['specification'] ?? '';
|
||||
$v['prev_cumulative'] = $v['prev_cumulative'] ?? 0;
|
||||
$v['today_count'] = $v['today_count'] ?? 0;
|
||||
$v['sort_order'] = (PmisWorkReportEquipment::where('report_id', $v['report_id'])->max('sort_order') ?? 0) + 1;
|
||||
|
||||
return response()->json(PmisWorkReportEquipment::create($v), 201);
|
||||
}
|
||||
|
||||
public function equipmentUpdate(Request $request, int $id): JsonResponse
|
||||
{
|
||||
$e = PmisWorkReportEquipment::where('tenant_id', $this->tenantId())->findOrFail($id);
|
||||
$e->update($request->validate([
|
||||
'equipment_name' => 'sometimes|string|max:200',
|
||||
'specification' => 'nullable|string|max:300',
|
||||
'prev_cumulative' => 'nullable|integer|min:0',
|
||||
'today_count' => 'nullable|integer|min:0',
|
||||
]));
|
||||
|
||||
return response()->json($e);
|
||||
}
|
||||
|
||||
public function equipmentDestroy(int $id): JsonResponse
|
||||
{
|
||||
PmisWorkReportEquipment::where('tenant_id', $this->tenantId())->findOrFail($id)->delete();
|
||||
|
||||
return response()->json(['message' => '삭제되었습니다.']);
|
||||
}
|
||||
|
||||
// ─── Material CRUD ───
|
||||
|
||||
public function materialStore(Request $request): JsonResponse
|
||||
{
|
||||
$v = $request->validate([
|
||||
'report_id' => 'required|integer|exists:pmis_daily_work_reports,id',
|
||||
'material_name' => 'required|string|max:200',
|
||||
'specification' => 'nullable|string|max:300',
|
||||
'unit' => 'nullable|string|max:50',
|
||||
'design_qty' => 'nullable|numeric|min:0',
|
||||
'prev_cumulative' => 'nullable|numeric|min:0',
|
||||
'today_count' => 'nullable|numeric|min:0',
|
||||
]);
|
||||
$v['tenant_id'] = $this->tenantId();
|
||||
$v['sort_order'] = (PmisWorkReportMaterial::where('report_id', $v['report_id'])->max('sort_order') ?? 0) + 1;
|
||||
|
||||
return response()->json(PmisWorkReportMaterial::create($v), 201);
|
||||
}
|
||||
|
||||
public function materialUpdate(Request $request, int $id): JsonResponse
|
||||
{
|
||||
$m = PmisWorkReportMaterial::where('tenant_id', $this->tenantId())->findOrFail($id);
|
||||
$m->update($request->validate([
|
||||
'material_name' => 'sometimes|string|max:200',
|
||||
'specification' => 'nullable|string|max:300',
|
||||
'unit' => 'nullable|string|max:50',
|
||||
'design_qty' => 'nullable|numeric|min:0',
|
||||
'prev_cumulative' => 'nullable|numeric|min:0',
|
||||
'today_count' => 'nullable|numeric|min:0',
|
||||
]));
|
||||
|
||||
return response()->json($m);
|
||||
}
|
||||
|
||||
public function materialDestroy(int $id): JsonResponse
|
||||
{
|
||||
PmisWorkReportMaterial::where('tenant_id', $this->tenantId())->findOrFail($id)->delete();
|
||||
|
||||
return response()->json(['message' => '삭제되었습니다.']);
|
||||
}
|
||||
|
||||
// ─── Volume CRUD ───
|
||||
|
||||
public function volumeStore(Request $request): JsonResponse
|
||||
{
|
||||
$v = $request->validate([
|
||||
'report_id' => 'required|integer|exists:pmis_daily_work_reports,id',
|
||||
'work_type' => 'required|string|max:200',
|
||||
'sub_work_type' => 'nullable|string|max:200',
|
||||
'unit' => 'nullable|string|max:50',
|
||||
'design_qty' => 'nullable|numeric|min:0',
|
||||
'prev_cumulative' => 'nullable|numeric|min:0',
|
||||
'today_count' => 'nullable|numeric|min:0',
|
||||
]);
|
||||
$v['tenant_id'] = $this->tenantId();
|
||||
$v['sort_order'] = (PmisWorkReportVolume::where('report_id', $v['report_id'])->max('sort_order') ?? 0) + 1;
|
||||
|
||||
return response()->json(PmisWorkReportVolume::create($v), 201);
|
||||
}
|
||||
|
||||
public function volumeUpdate(Request $request, int $id): JsonResponse
|
||||
{
|
||||
$vol = PmisWorkReportVolume::where('tenant_id', $this->tenantId())->findOrFail($id);
|
||||
$vol->update($request->validate([
|
||||
'work_type' => 'sometimes|string|max:200',
|
||||
'sub_work_type' => 'nullable|string|max:200',
|
||||
'unit' => 'nullable|string|max:50',
|
||||
'design_qty' => 'nullable|numeric|min:0',
|
||||
'prev_cumulative' => 'nullable|numeric|min:0',
|
||||
'today_count' => 'nullable|numeric|min:0',
|
||||
]));
|
||||
|
||||
return response()->json($vol);
|
||||
}
|
||||
|
||||
public function volumeDestroy(int $id): JsonResponse
|
||||
{
|
||||
PmisWorkReportVolume::where('tenant_id', $this->tenantId())->findOrFail($id)->delete();
|
||||
|
||||
return response()->json(['message' => '삭제되었습니다.']);
|
||||
}
|
||||
|
||||
// ─── Photo CRUD ───
|
||||
|
||||
public function photoStore(Request $request): JsonResponse
|
||||
{
|
||||
$v = $request->validate([
|
||||
'report_id' => 'required|integer|exists:pmis_daily_work_reports,id',
|
||||
'location' => 'nullable|string|max:200',
|
||||
'content' => 'nullable|string|max:500',
|
||||
'photo' => 'nullable|image|max:10240',
|
||||
]);
|
||||
|
||||
$path = '';
|
||||
if ($request->hasFile('photo')) {
|
||||
$path = $request->file('photo')->store('pmis/work-report-photos', 'public');
|
||||
}
|
||||
|
||||
$photo = PmisWorkReportPhoto::create([
|
||||
'tenant_id' => $this->tenantId(),
|
||||
'report_id' => $v['report_id'],
|
||||
'photo_path' => $path,
|
||||
'location' => $v['location'] ?? '',
|
||||
'content' => $v['content'] ?? '',
|
||||
'photo_date' => now()->toDateString(),
|
||||
'sort_order' => (PmisWorkReportPhoto::where('report_id', $v['report_id'])->max('sort_order') ?? 0) + 1,
|
||||
]);
|
||||
|
||||
return response()->json($photo, 201);
|
||||
}
|
||||
|
||||
public function photoDestroy(int $id): JsonResponse
|
||||
{
|
||||
PmisWorkReportPhoto::where('tenant_id', $this->tenantId())->findOrFail($id)->delete();
|
||||
|
||||
return response()->json(['message' => '삭제되었습니다.']);
|
||||
}
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Juil;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Juil\PmisEquipment;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class PmisEquipmentController extends Controller
|
||||
{
|
||||
private function tenantId(): int
|
||||
{
|
||||
return (int) session('current_tenant_id', 1);
|
||||
}
|
||||
|
||||
public function list(Request $request): JsonResponse
|
||||
{
|
||||
$query = PmisEquipment::tenant($this->tenantId())
|
||||
->orderByDesc('id');
|
||||
|
||||
if ($request->filled('company')) {
|
||||
$query->where('company_name', 'like', '%' . $request->company . '%');
|
||||
}
|
||||
if ($request->filled('equipment_name')) {
|
||||
$query->where('equipment_name', 'like', '%' . $request->equipment_name . '%');
|
||||
}
|
||||
if ($request->filled('search')) {
|
||||
$s = $request->search;
|
||||
$query->where(function ($q) use ($s) {
|
||||
$q->where('equipment_name', 'like', "%{$s}%")
|
||||
->orWhere('equipment_code', 'like', "%{$s}%")
|
||||
->orWhere('equipment_number', 'like', "%{$s}%")
|
||||
->orWhere('operator', 'like', "%{$s}%");
|
||||
});
|
||||
}
|
||||
|
||||
$perPage = $request->integer('per_page', 15);
|
||||
$equipments = $query->paginate($perPage);
|
||||
|
||||
return response()->json($equipments);
|
||||
}
|
||||
|
||||
public function store(Request $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'company_name' => 'required|string|max:200',
|
||||
'equipment_code' => 'nullable|string|max:50',
|
||||
'equipment_name' => 'required|string|max:200',
|
||||
'specification' => 'nullable|string|max:300',
|
||||
'unit' => 'nullable|string|max:50',
|
||||
'equipment_number' => 'required|string|max:100',
|
||||
'operator' => 'nullable|string|max:50',
|
||||
'inspection_end_date' => 'nullable|date',
|
||||
'inspection_not_applicable' => 'nullable|boolean',
|
||||
'insurance_end_date' => 'nullable|date',
|
||||
'insurance_not_applicable' => 'nullable|boolean',
|
||||
]);
|
||||
|
||||
$validated['tenant_id'] = $this->tenantId();
|
||||
$validated['inspection_not_applicable'] = $validated['inspection_not_applicable'] ?? false;
|
||||
$validated['insurance_not_applicable'] = $validated['insurance_not_applicable'] ?? false;
|
||||
|
||||
$equipment = PmisEquipment::create($validated);
|
||||
|
||||
return response()->json($equipment, 201);
|
||||
}
|
||||
|
||||
public function update(Request $request, int $id): JsonResponse
|
||||
{
|
||||
$equipment = PmisEquipment::tenant($this->tenantId())->findOrFail($id);
|
||||
|
||||
$validated = $request->validate([
|
||||
'company_name' => 'sometimes|required|string|max:200',
|
||||
'equipment_code' => 'nullable|string|max:50',
|
||||
'equipment_name' => 'sometimes|required|string|max:200',
|
||||
'specification' => 'nullable|string|max:300',
|
||||
'unit' => 'nullable|string|max:50',
|
||||
'equipment_number' => 'sometimes|required|string|max:100',
|
||||
'operator' => 'nullable|string|max:50',
|
||||
'inspection_end_date' => 'nullable|date',
|
||||
'inspection_not_applicable' => 'nullable|boolean',
|
||||
'insurance_end_date' => 'nullable|date',
|
||||
'insurance_not_applicable' => 'nullable|boolean',
|
||||
]);
|
||||
|
||||
$equipment->update($validated);
|
||||
|
||||
return response()->json($equipment);
|
||||
}
|
||||
|
||||
public function destroy(int $id): JsonResponse
|
||||
{
|
||||
$equipment = PmisEquipment::tenant($this->tenantId())->findOrFail($id);
|
||||
$equipment->delete();
|
||||
|
||||
return response()->json(['message' => '삭제되었습니다.']);
|
||||
}
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Juil;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Juil\PmisMaterial;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class PmisMaterialController extends Controller
|
||||
{
|
||||
private function tenantId(): int
|
||||
{
|
||||
return (int) session('current_tenant_id', 1);
|
||||
}
|
||||
|
||||
public function list(Request $request): JsonResponse
|
||||
{
|
||||
$query = PmisMaterial::tenant($this->tenantId())
|
||||
->orderByDesc('id');
|
||||
|
||||
if ($request->filled('company')) {
|
||||
$query->where('company_name', 'like', '%' . $request->company . '%');
|
||||
}
|
||||
if ($request->filled('material_name')) {
|
||||
$query->where('material_name', 'like', '%' . $request->material_name . '%');
|
||||
}
|
||||
if ($request->filled('search')) {
|
||||
$s = $request->search;
|
||||
$query->where(function ($q) use ($s) {
|
||||
$q->where('material_name', 'like', "%{$s}%")
|
||||
->orWhere('material_code', 'like', "%{$s}%")
|
||||
->orWhere('specification', 'like', "%{$s}%");
|
||||
});
|
||||
}
|
||||
|
||||
$perPage = $request->integer('per_page', 15);
|
||||
$materials = $query->paginate($perPage);
|
||||
|
||||
return response()->json($materials);
|
||||
}
|
||||
|
||||
public function store(Request $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'company_name' => 'required|string|max:200',
|
||||
'material_code' => 'nullable|string|max:50',
|
||||
'material_name' => 'required|string|max:200',
|
||||
'specification' => 'nullable|string|max:300',
|
||||
'unit' => 'nullable|string|max:50',
|
||||
'design_quantity' => 'nullable|numeric|min:0',
|
||||
]);
|
||||
|
||||
$validated['tenant_id'] = $this->tenantId();
|
||||
$validated['design_quantity'] = $validated['design_quantity'] ?? 0;
|
||||
|
||||
$material = PmisMaterial::create($validated);
|
||||
|
||||
return response()->json($material, 201);
|
||||
}
|
||||
|
||||
public function update(Request $request, int $id): JsonResponse
|
||||
{
|
||||
$material = PmisMaterial::tenant($this->tenantId())->findOrFail($id);
|
||||
|
||||
$validated = $request->validate([
|
||||
'company_name' => 'sometimes|required|string|max:200',
|
||||
'material_code' => 'nullable|string|max:50',
|
||||
'material_name' => 'sometimes|required|string|max:200',
|
||||
'specification' => 'nullable|string|max:300',
|
||||
'unit' => 'nullable|string|max:50',
|
||||
'design_quantity' => 'nullable|numeric|min:0',
|
||||
]);
|
||||
|
||||
$material->update($validated);
|
||||
|
||||
return response()->json($material);
|
||||
}
|
||||
|
||||
public function destroy(int $id): JsonResponse
|
||||
{
|
||||
$material = PmisMaterial::tenant($this->tenantId())->findOrFail($id);
|
||||
$material->delete();
|
||||
|
||||
return response()->json(['message' => '삭제되었습니다.']);
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Juil;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Juil\PmisWorkVolume;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class PmisWorkVolumeController extends Controller
|
||||
{
|
||||
private function tenantId(): int
|
||||
{
|
||||
return (int) session('current_tenant_id', 1);
|
||||
}
|
||||
|
||||
public function list(Request $request): JsonResponse
|
||||
{
|
||||
$query = PmisWorkVolume::tenant($this->tenantId())
|
||||
->orderByDesc('id');
|
||||
|
||||
if ($request->filled('search')) {
|
||||
$s = $request->search;
|
||||
$query->where(function ($q) use ($s) {
|
||||
$q->where('work_type', 'like', "%{$s}%")
|
||||
->orWhere('sub_work_type', 'like', "%{$s}%");
|
||||
});
|
||||
}
|
||||
|
||||
$perPage = $request->integer('per_page', 15);
|
||||
$workVolumes = $query->paginate($perPage);
|
||||
|
||||
return response()->json($workVolumes);
|
||||
}
|
||||
|
||||
public function store(Request $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'work_type' => 'required|string|max:200',
|
||||
'sub_work_type' => 'required|string|max:200',
|
||||
'unit' => 'required|string|max:50',
|
||||
'design_quantity' => 'nullable|numeric|min:0',
|
||||
'daily_report_applied' => 'nullable|boolean',
|
||||
]);
|
||||
|
||||
$validated['tenant_id'] = $this->tenantId();
|
||||
$validated['design_quantity'] = $validated['design_quantity'] ?? 0;
|
||||
$validated['daily_report_applied'] = $validated['daily_report_applied'] ?? false;
|
||||
|
||||
$workVolume = PmisWorkVolume::create($validated);
|
||||
|
||||
return response()->json($workVolume, 201);
|
||||
}
|
||||
|
||||
public function update(Request $request, int $id): JsonResponse
|
||||
{
|
||||
$workVolume = PmisWorkVolume::tenant($this->tenantId())->findOrFail($id);
|
||||
|
||||
$validated = $request->validate([
|
||||
'work_type' => 'sometimes|required|string|max:200',
|
||||
'sub_work_type' => 'sometimes|required|string|max:200',
|
||||
'unit' => 'sometimes|required|string|max:50',
|
||||
'design_quantity' => 'nullable|numeric|min:0',
|
||||
'daily_report_applied' => 'nullable|boolean',
|
||||
]);
|
||||
|
||||
$workVolume->update($validated);
|
||||
|
||||
return response()->json($workVolume);
|
||||
}
|
||||
|
||||
public function destroy(int $id): JsonResponse
|
||||
{
|
||||
$workVolume = PmisWorkVolume::tenant($this->tenantId())->findOrFail($id);
|
||||
$workVolume->delete();
|
||||
|
||||
return response()->json(['message' => '삭제되었습니다.']);
|
||||
}
|
||||
}
|
||||
@@ -1,133 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Juil;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Juil\PmisConstructionWorker;
|
||||
use App\Models\Juil\PmisJobType;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class PmisWorkforceController extends Controller
|
||||
{
|
||||
private function tenantId(): int
|
||||
{
|
||||
return (int) session('current_tenant_id', 1);
|
||||
}
|
||||
|
||||
// ── 인원 CRUD ──
|
||||
|
||||
public function workerList(Request $request): JsonResponse
|
||||
{
|
||||
$query = PmisConstructionWorker::tenant($this->tenantId())
|
||||
->with('jobType:id,name')
|
||||
->orderByDesc('id');
|
||||
|
||||
if ($request->filled('company')) {
|
||||
$query->where('company_name', 'like', '%' . $request->company . '%');
|
||||
}
|
||||
if ($request->filled('trade')) {
|
||||
$query->where('trade_name', $request->trade);
|
||||
}
|
||||
if ($request->filled('job_type_id')) {
|
||||
$query->where('job_type_id', $request->job_type_id);
|
||||
}
|
||||
if ($request->filled('search')) {
|
||||
$s = $request->search;
|
||||
$query->where(function ($q) use ($s) {
|
||||
$q->where('name', 'like', "%{$s}%")
|
||||
->orWhere('phone', 'like', "%{$s}%");
|
||||
});
|
||||
}
|
||||
|
||||
$perPage = $request->integer('per_page', 15);
|
||||
$workers = $query->paginate($perPage);
|
||||
|
||||
return response()->json($workers);
|
||||
}
|
||||
|
||||
public function workerStore(Request $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'company_name' => 'required|string|max:200',
|
||||
'trade_name' => 'required|string|max:100',
|
||||
'job_type_id' => 'nullable|exists:pmis_job_types,id',
|
||||
'name' => 'required|string|max:50',
|
||||
'phone' => 'nullable|string|max:20',
|
||||
'birth_date' => 'nullable|string|max:6',
|
||||
'ssn_gender' => 'nullable|string|max:1',
|
||||
'wage' => 'nullable|integer|min:0',
|
||||
'blood_type' => 'nullable|string|max:5',
|
||||
'remark' => 'nullable|string|max:500',
|
||||
]);
|
||||
|
||||
$validated['tenant_id'] = $this->tenantId();
|
||||
$validated['wage'] = $validated['wage'] ?? 0;
|
||||
|
||||
$worker = PmisConstructionWorker::create($validated);
|
||||
$worker->load('jobType:id,name');
|
||||
|
||||
return response()->json($worker, 201);
|
||||
}
|
||||
|
||||
public function workerUpdate(Request $request, int $id): JsonResponse
|
||||
{
|
||||
$worker = PmisConstructionWorker::tenant($this->tenantId())->findOrFail($id);
|
||||
|
||||
$validated = $request->validate([
|
||||
'company_name' => 'sometimes|required|string|max:200',
|
||||
'trade_name' => 'sometimes|required|string|max:100',
|
||||
'job_type_id' => 'nullable|exists:pmis_job_types,id',
|
||||
'name' => 'sometimes|required|string|max:50',
|
||||
'phone' => 'nullable|string|max:20',
|
||||
'birth_date' => 'nullable|string|max:6',
|
||||
'ssn_gender' => 'nullable|string|max:1',
|
||||
'wage' => 'nullable|integer|min:0',
|
||||
'blood_type' => 'nullable|string|max:5',
|
||||
'remark' => 'nullable|string|max:500',
|
||||
]);
|
||||
|
||||
$worker->update($validated);
|
||||
$worker->load('jobType:id,name');
|
||||
|
||||
return response()->json($worker);
|
||||
}
|
||||
|
||||
public function workerDestroy(int $id): JsonResponse
|
||||
{
|
||||
$worker = PmisConstructionWorker::tenant($this->tenantId())->findOrFail($id);
|
||||
$worker->delete();
|
||||
|
||||
return response()->json(['message' => '삭제되었습니다.']);
|
||||
}
|
||||
|
||||
// ── 직종 CRUD ──
|
||||
|
||||
public function jobTypeList(): JsonResponse
|
||||
{
|
||||
$types = PmisJobType::tenant($this->tenantId())
|
||||
->where('is_active', true)
|
||||
->orderBy('sort_order')
|
||||
->orderBy('name')
|
||||
->get(['id', 'name', 'sort_order']);
|
||||
|
||||
return response()->json($types);
|
||||
}
|
||||
|
||||
public function jobTypeStore(Request $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'name' => 'required|string|max:100',
|
||||
]);
|
||||
|
||||
$maxSort = PmisJobType::tenant($this->tenantId())->max('sort_order') ?? 0;
|
||||
|
||||
$type = PmisJobType::create([
|
||||
'tenant_id' => $this->tenantId(),
|
||||
'name' => $validated['name'],
|
||||
'sort_order' => $maxSort + 1,
|
||||
]);
|
||||
|
||||
return response()->json($type, 201);
|
||||
}
|
||||
}
|
||||
@@ -6,8 +6,6 @@
|
||||
use App\Models\Admin\AdminPmTask;
|
||||
use App\Services\ProjectManagement\ImportService;
|
||||
use App\Services\ProjectManagement\ProjectService;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class ProjectManagementController extends Controller
|
||||
@@ -19,13 +17,8 @@ public function __construct(
|
||||
/**
|
||||
* 프로젝트 관리 대시보드
|
||||
*/
|
||||
public function index(Request $request): View|Response
|
||||
public function index(): View
|
||||
{
|
||||
// HTMX 부분 로드 시 @push('scripts')가 실행되지 않으므로 전체 페이지 리로드
|
||||
if ($request->header('HX-Request')) {
|
||||
return response('', 200)->header('HX-Redirect', route('pm.index'));
|
||||
}
|
||||
|
||||
$summary = $this->projectService->getDashboardSummary();
|
||||
$statuses = AdminPmProject::getStatuses();
|
||||
$taskStatuses = AdminPmTask::getStatuses();
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace App\Http\Controllers\Rd;
|
||||
|
||||
use App\Helpers\AiTokenHelper;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Rd\CmSong;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
@@ -115,10 +114,6 @@ public function generateLyrics(Request $request): JsonResponse
|
||||
}
|
||||
|
||||
$data = $response->json();
|
||||
|
||||
// 토큰 사용량 기록
|
||||
AiTokenHelper::saveGeminiUsage($data, 'gemini-2.5-flash', '나레이션-가사생성');
|
||||
|
||||
$text = $data['candidates'][0]['content']['parts'][0]['text'] ?? '';
|
||||
|
||||
return response()->json([
|
||||
@@ -170,10 +165,6 @@ public function generateAudio(Request $request): JsonResponse
|
||||
}
|
||||
|
||||
$data = $response->json();
|
||||
|
||||
// 토큰 사용량 기록
|
||||
AiTokenHelper::saveGeminiUsage($data, 'gemini-2.5-flash-preview-tts', '나레이션-TTS');
|
||||
|
||||
$inlineData = $data['candidates'][0]['content']['parts'][0]['inlineData'] ?? null;
|
||||
|
||||
if (! $inlineData || empty($inlineData['data'])) {
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Helpers\AiTokenHelper;
|
||||
use App\Models\HR\Employee;
|
||||
use App\Models\Rd\AiQuotation;
|
||||
use App\Models\Tenants\Department;
|
||||
@@ -10,8 +9,6 @@
|
||||
use App\Services\Rd\AiQuotationService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class RdController extends Controller
|
||||
@@ -304,325 +301,4 @@ public function editQuotation(Request $request, int $id): View|\Illuminate\Http\
|
||||
|
||||
return view('rd.ai-quotation.edit', compact('quotation'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 기획디자인 - 플래닝 캔버스
|
||||
*/
|
||||
public function planningDesign(Request $request): View|\Illuminate\Http\Response
|
||||
{
|
||||
if ($request->header('HX-Request')) {
|
||||
return response('', 200)->header('HX-Redirect', route('rd.planning-design'));
|
||||
}
|
||||
|
||||
return view('rd.planning-design.index');
|
||||
}
|
||||
|
||||
/**
|
||||
* 디자인 인사이트 - UI/UX 연구 도구
|
||||
*/
|
||||
public function designInsight(Request $request): View|\Illuminate\Http\Response
|
||||
{
|
||||
if ($request->header('HX-Request')) {
|
||||
return response('', 200)->header('HX-Redirect', route('rd.design-insight'));
|
||||
}
|
||||
|
||||
return view('rd.design-insight.index');
|
||||
}
|
||||
|
||||
/**
|
||||
* 사운드 로고 생성기
|
||||
*/
|
||||
public function soundLogo(Request $request): View|\Illuminate\Http\Response
|
||||
{
|
||||
if ($request->header('HX-Request')) {
|
||||
return response('', 200)->header('HX-Redirect', route('rd.sound-logo'));
|
||||
}
|
||||
|
||||
return view('rd.sound-logo.index');
|
||||
}
|
||||
|
||||
/**
|
||||
* Lyria RealTime 접속용 API 설정 반환
|
||||
*/
|
||||
public function soundLogoLyriaConfig(): JsonResponse
|
||||
{
|
||||
$apiKey = config('services.gemini.api_key');
|
||||
|
||||
if (! $apiKey) {
|
||||
return response()->json(['success' => false, 'error' => 'API 키가 설정되지 않았습니다.'], 500);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'api_key' => $apiKey,
|
||||
'ws_url' => 'wss://generativelanguage.googleapis.com/ws/google.ai.generativelanguage.v1alpha.GenerativeService.BidiGenerateMusic',
|
||||
'model' => 'models/lyria-realtime-exp',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 사운드 로고 AI 생성 (Gemini API)
|
||||
*/
|
||||
public function soundLogoGenerate(Request $request): JsonResponse
|
||||
{
|
||||
$request->validate([
|
||||
'prompt' => 'required|string|max:500',
|
||||
'category' => 'nullable|string',
|
||||
'duration' => 'nullable|numeric|min:0.3|max:5',
|
||||
]);
|
||||
|
||||
$apiKey = config('services.gemini.api_key');
|
||||
$baseUrl = config('services.gemini.base_url', 'https://generativelanguage.googleapis.com/v1beta');
|
||||
$model = config('services.gemini.model', 'gemini-2.5-flash');
|
||||
|
||||
if (! $apiKey) {
|
||||
return response()->json(['success' => false, 'error' => 'Gemini API 키가 설정되지 않았습니다.'], 500);
|
||||
}
|
||||
|
||||
$category = $request->category ?? '기업 시그널';
|
||||
$duration = $request->duration ?? 1.5;
|
||||
|
||||
$prompt = <<<PROMPT
|
||||
당신은 사운드 디자인 전문가입니다. 사용자의 요청에 맞는 사운드 로고(짧은 시그니처 사운드)를 Web Audio API 음표 시퀀스로 설계해주세요.
|
||||
|
||||
## 사용자 요청
|
||||
- 설명: {$request->prompt}
|
||||
- 카테고리: {$category}
|
||||
- 목표 길이: {$duration}초
|
||||
|
||||
## 사용 가능한 음표
|
||||
C3, C#3, D3, D#3, E3, F3, F#3, G3, G#3, A3, A#3, B3,
|
||||
C4, C#4, D4, D#4, E4, F4, F#4, G4, G#4, A4, A#4, B4,
|
||||
C5, C#5, D5, D#5, E5, F5, F#5, G5, G#5, A5, A#5, B5, C6
|
||||
|
||||
## 음표 타입
|
||||
- note: 단일 음 (note 필드 필수)
|
||||
- chord: 화음 (chord 배열 필수, 2~4개 음)
|
||||
- rest: 쉼표 (duration만 필요)
|
||||
|
||||
## 신스 타입
|
||||
- sine: 부드러움 (기업 로고, 알림에 적합)
|
||||
- triangle: 따뜻함 (성공, 게임에 적합)
|
||||
- square: 8bit/디지털 (게임, UI에 적합)
|
||||
- sawtooth: 날카로움 (록, 긴급 알림에 적합)
|
||||
|
||||
## 반드시 아래 JSON 형식으로만 응답하세요
|
||||
{
|
||||
"name": "사운드 이름",
|
||||
"desc": "사운드 설명 (한줄)",
|
||||
"synth": "sine",
|
||||
"adsr": { "attack": 10, "decay": 80, "sustain": 0.6, "release": 400 },
|
||||
"volume": 0.8,
|
||||
"reverb": 0.3,
|
||||
"notes": [
|
||||
{ "type": "note", "note": "C5", "duration": 0.20, "velocity": 0.8 },
|
||||
{ "type": "rest", "duration": 0.10 },
|
||||
{ "type": "chord", "chord": ["C4", "E4", "G4"], "duration": 0.50, "velocity": 1.0 }
|
||||
]
|
||||
}
|
||||
|
||||
## 설계 원칙
|
||||
- 음표의 duration 합계가 목표 길이({$duration}초)에 근접하도록 설계
|
||||
- velocity: 0.3~1.0 (음의 강약으로 표현력 추가)
|
||||
- ADSR: attack(1~500ms), decay(10~1000ms), sustain(0~1.0), release(10~3000ms)
|
||||
- 카테고리 특성에 맞는 synth와 ADSR 선택
|
||||
- 음악적으로 조화롭고 기억에 남는 멜로디 설계
|
||||
- 최소 2개, 최대 12개 음표 사용
|
||||
- name은 10자 이내, desc는 30자 이내로 간결하게 작성
|
||||
PROMPT;
|
||||
|
||||
try {
|
||||
$response = Http::timeout(30)->post(
|
||||
"{$baseUrl}/models/{$model}:generateContent?key={$apiKey}",
|
||||
[
|
||||
'contents' => [
|
||||
['parts' => [['text' => $prompt]]],
|
||||
],
|
||||
'generationConfig' => [
|
||||
'temperature' => 0.7,
|
||||
'maxOutputTokens' => 4096,
|
||||
'responseMimeType' => 'application/json',
|
||||
],
|
||||
]
|
||||
);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('SoundLogo AI 생성 실패', ['error' => $e->getMessage()]);
|
||||
|
||||
return response()->json(['success' => false, 'error' => 'AI 서버 연결 실패'], 500);
|
||||
}
|
||||
|
||||
if (! $response->successful()) {
|
||||
Log::error('SoundLogo AI API 오류', ['status' => $response->status(), 'body' => $response->body()]);
|
||||
|
||||
return response()->json(['success' => false, 'error' => 'AI 생성 실패: '.$response->status()], 500);
|
||||
}
|
||||
|
||||
$data = $response->json();
|
||||
|
||||
// 토큰 사용량 기록
|
||||
AiTokenHelper::saveGeminiUsage($data, $model, '사운드로고-AI생성');
|
||||
|
||||
$text = $data['candidates'][0]['content']['parts'][0]['text'] ?? '';
|
||||
|
||||
// JSON 파싱 (코드블록 제거)
|
||||
$text = preg_replace('/^```(?:json)?\s*/m', '', $text);
|
||||
$text = preg_replace('/```\s*$/m', '', $text);
|
||||
$result = json_decode(trim($text), true);
|
||||
|
||||
if (! $result || ! isset($result['notes'])) {
|
||||
Log::warning('SoundLogo AI 응답 파싱 실패', ['text' => substr($text, 0, 500)]);
|
||||
|
||||
return response()->json(['success' => false, 'error' => 'AI 응답을 파싱할 수 없습니다.'], 500);
|
||||
}
|
||||
|
||||
return response()->json(['success' => true, 'data' => $result]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 사운드 로고 TTS 음성 생성 (Gemini TTS API)
|
||||
*/
|
||||
public function soundLogoTts(Request $request): JsonResponse
|
||||
{
|
||||
$request->validate([
|
||||
'text' => 'required|string|max:200',
|
||||
'voice_name' => 'nullable|string|max:30',
|
||||
'voice_category' => 'nullable|string|in:female,male,child',
|
||||
'voice_style' => 'nullable|string|max:100',
|
||||
'voice_speed' => 'nullable|integer|min:1|max:5',
|
||||
]);
|
||||
|
||||
$apiKey = config('services.gemini.api_key');
|
||||
$baseUrl = config('services.gemini.base_url', 'https://generativelanguage.googleapis.com/v1beta');
|
||||
|
||||
if (! $apiKey) {
|
||||
return response()->json(['success' => false, 'error' => 'Gemini API 키가 설정되지 않았습니다.'], 500);
|
||||
}
|
||||
|
||||
$voiceName = $request->voice_name ?: 'Kore';
|
||||
$voiceCategory = $request->voice_category ?: 'female';
|
||||
$voiceStyle = $request->voice_style ?: '';
|
||||
$voiceSpeed = $request->voice_speed ?: 3;
|
||||
|
||||
// 속도 지시문 매핑
|
||||
$speedDirectives = [
|
||||
1 => '아주 천천히 또박또박 말해주세요.',
|
||||
2 => '조금 느린 속도로 말해주세요.',
|
||||
3 => '',
|
||||
4 => '조금 빠른 속도로 말해주세요.',
|
||||
5 => '아주 빠른 속도로 말해주세요.',
|
||||
];
|
||||
|
||||
// TTS 프롬프트 구성 — Director's Note 형식
|
||||
$ttsText = $request->text;
|
||||
$notes = [];
|
||||
|
||||
// 아이 카테고리: 높은 톤으로 어린이처럼 연기하도록 강한 지시문
|
||||
if ($voiceCategory === 'child') {
|
||||
$notes[] = 'Speak as a young child with a high-pitched, innocent voice';
|
||||
}
|
||||
|
||||
if ($voiceStyle) {
|
||||
$notes[] = $voiceStyle;
|
||||
}
|
||||
|
||||
if (! empty($speedDirectives[$voiceSpeed])) {
|
||||
$notes[] = $speedDirectives[$voiceSpeed];
|
||||
}
|
||||
|
||||
if (! empty($notes)) {
|
||||
$direction = implode('. ', $notes);
|
||||
$ttsText = "[{$direction}]\n\n{$ttsText}";
|
||||
}
|
||||
|
||||
// 짧은 텍스트는 TTS 모델이 텍스트 생성으로 인식할 수 있으므로 발화 컨텍스트 추가
|
||||
if (mb_strlen($ttsText) < 4) {
|
||||
$ttsText = "'{$ttsText}' ";
|
||||
}
|
||||
|
||||
try {
|
||||
$response = Http::timeout(30)->post(
|
||||
"{$baseUrl}/models/gemini-2.5-flash-preview-tts:generateContent?key={$apiKey}",
|
||||
[
|
||||
'contents' => [
|
||||
['parts' => [['text' => $ttsText]]],
|
||||
],
|
||||
'generationConfig' => [
|
||||
'responseModalities' => ['AUDIO'],
|
||||
'speechConfig' => [
|
||||
'voiceConfig' => [
|
||||
'prebuiltVoiceConfig' => [
|
||||
'voiceName' => $voiceName,
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
]
|
||||
);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('SoundLogo TTS 생성 실패', ['error' => $e->getMessage()]);
|
||||
|
||||
return response()->json(['success' => false, 'error' => 'TTS 서버 연결 실패'], 500);
|
||||
}
|
||||
|
||||
if (! $response->successful()) {
|
||||
$body = $response->json();
|
||||
$msg = $body['error']['message'] ?? ('TTS 생성 실패: '.$response->status());
|
||||
Log::warning('SoundLogo TTS API 에러', ['status' => $response->status(), 'body' => $msg]);
|
||||
|
||||
return response()->json(['success' => false, 'error' => $msg], $response->status());
|
||||
}
|
||||
|
||||
$data = $response->json();
|
||||
|
||||
// 토큰 사용량 기록
|
||||
AiTokenHelper::saveGeminiUsage($data, 'gemini-2.5-flash-preview-tts', '사운드로고-TTS');
|
||||
|
||||
$inlineData = $data['candidates'][0]['content']['parts'][0]['inlineData'] ?? null;
|
||||
|
||||
if (! $inlineData || empty($inlineData['data'])) {
|
||||
return response()->json(['success' => false, 'error' => '음성 데이터를 받지 못했습니다.'], 500);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'audio_data' => $inlineData['data'],
|
||||
'mime_type' => $inlineData['mimeType'] ?? 'audio/L16;rate=24000',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 자동도면 생성 (전개도 시뮬레이터)
|
||||
*/
|
||||
public function autoDrawing(Request $request): View|\Illuminate\Http\Response
|
||||
{
|
||||
if ($request->header('HX-Request')) {
|
||||
return response('', 200)->header('HX-Redirect', route('rd.auto-drawing'));
|
||||
}
|
||||
|
||||
return view('rd.auto-drawing.index');
|
||||
}
|
||||
|
||||
/**
|
||||
* 방화셔터 도면생성
|
||||
*/
|
||||
public function fireShutterDrawing(Request $request): View|\Illuminate\Http\Response
|
||||
{
|
||||
if ($request->header('HX-Request')) {
|
||||
return response('', 200)->header('HX-Redirect', route('rd.fire-shutter-drawing'));
|
||||
}
|
||||
|
||||
return view('rd.fire-shutter-drawing.index');
|
||||
}
|
||||
|
||||
/**
|
||||
* 클코 → 슬랙 변환기
|
||||
*/
|
||||
public function ccToSlack(Request $request): View|\Illuminate\Http\Response
|
||||
{
|
||||
if ($request->header('HX-Request')) {
|
||||
return response('', 200)->header('HX-Redirect', route('rd.cc-to-slack'));
|
||||
}
|
||||
|
||||
return view('rd.cc-to-slack.index');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,250 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\System;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Tenants\MailLog;
|
||||
use App\Models\Tenants\Tenant;
|
||||
use App\Models\Tenants\TenantMailConfig;
|
||||
use App\Services\Mail\SmtpConnectionTester;
|
||||
use App\Services\Mail\TenantMailService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class TenantMailConfigController extends Controller
|
||||
{
|
||||
/**
|
||||
* 전체 테넌트 메일 설정 현황
|
||||
*/
|
||||
public function index(Request $request): View|Response
|
||||
{
|
||||
if ($request->header('HX-Request')) {
|
||||
return response('', 200)->header('HX-Redirect', route('system.tenant-mail.index'));
|
||||
}
|
||||
|
||||
$tenants = Tenant::orderBy('company_name')
|
||||
->get()
|
||||
->map(function ($tenant) {
|
||||
$config = TenantMailConfig::withoutGlobalScopes()
|
||||
->where('tenant_id', $tenant->id)
|
||||
->first();
|
||||
|
||||
$todayCount = MailLog::withoutGlobalScopes()
|
||||
->where('tenant_id', $tenant->id)
|
||||
->whereDate('created_at', today())
|
||||
->whereIn('status', ['queued', 'sent'])
|
||||
->count();
|
||||
|
||||
return (object) [
|
||||
'tenant' => $tenant,
|
||||
'config' => $config,
|
||||
'today_count' => $todayCount,
|
||||
];
|
||||
});
|
||||
|
||||
return view('system.tenant-mail.index', compact('tenants'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 테넌트 메일 설정 편집 폼
|
||||
*/
|
||||
public function edit(Request $request, int $tenantId): View|Response
|
||||
{
|
||||
if ($request->header('HX-Request')) {
|
||||
return response('', 200)->header('HX-Redirect', route('system.tenant-mail.edit', $tenantId));
|
||||
}
|
||||
|
||||
$tenant = Tenant::findOrFail($tenantId);
|
||||
|
||||
$config = TenantMailConfig::withoutGlobalScopes()
|
||||
->where('tenant_id', $tenantId)
|
||||
->first();
|
||||
|
||||
$presets = config('mail-presets', []);
|
||||
|
||||
$mailService = app(TenantMailService::class);
|
||||
$todayCount = $mailService->getTodayCount($tenantId);
|
||||
$monthCount = $mailService->getMonthCount($tenantId);
|
||||
|
||||
return view('system.tenant-mail.edit', compact(
|
||||
'tenant',
|
||||
'config',
|
||||
'presets',
|
||||
'todayCount',
|
||||
'monthCount'
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* 메일 설정 저장
|
||||
*/
|
||||
public function update(Request $request, int $tenantId): JsonResponse
|
||||
{
|
||||
$tenant = Tenant::findOrFail($tenantId);
|
||||
|
||||
$validated = $request->validate([
|
||||
'provider' => 'required|string|in:platform,smtp',
|
||||
'from_name' => 'required|string|max:255',
|
||||
'from_address' => 'required|email|max:255',
|
||||
'reply_to' => 'nullable|email|max:255',
|
||||
'daily_limit' => 'required|integer|min:1|max:99999',
|
||||
'is_active' => 'boolean',
|
||||
// SMTP 설정
|
||||
'preset' => 'nullable|string',
|
||||
'smtp_host' => 'required_if:provider,smtp|nullable|string|max:255',
|
||||
'smtp_port' => 'required_if:provider,smtp|nullable|integer|min:1|max:65535',
|
||||
'smtp_encryption' => 'required_if:provider,smtp|nullable|string|in:tls,ssl',
|
||||
'smtp_username' => 'required_if:provider,smtp|nullable|string|max:255',
|
||||
'smtp_password' => 'nullable|string|max:255',
|
||||
// 브랜딩
|
||||
'branding_company_name' => 'nullable|string|max:255',
|
||||
'branding_primary_color' => 'nullable|string|max:7',
|
||||
'branding_company_address' => 'nullable|string|max:500',
|
||||
'branding_company_phone' => 'nullable|string|max:50',
|
||||
'branding_footer_text' => 'nullable|string|max:500',
|
||||
]);
|
||||
|
||||
$config = TenantMailConfig::withoutGlobalScopes()
|
||||
->where('tenant_id', $tenantId)
|
||||
->first();
|
||||
|
||||
$data = [
|
||||
'tenant_id' => $tenantId,
|
||||
'provider' => $validated['provider'],
|
||||
'from_name' => $validated['from_name'],
|
||||
'from_address' => $validated['from_address'],
|
||||
'reply_to' => $validated['reply_to'] ?? null,
|
||||
'daily_limit' => $validated['daily_limit'],
|
||||
'is_active' => $validated['is_active'] ?? true,
|
||||
];
|
||||
|
||||
// Options 구성
|
||||
$options = $config?->options ?? [];
|
||||
|
||||
// SMTP 설정
|
||||
if ($validated['provider'] === 'smtp') {
|
||||
$options['preset'] = $validated['preset'] ?? 'custom';
|
||||
$options['smtp'] = [
|
||||
'host' => $validated['smtp_host'],
|
||||
'port' => $validated['smtp_port'],
|
||||
'encryption' => $validated['smtp_encryption'],
|
||||
'username' => $validated['smtp_username'],
|
||||
];
|
||||
|
||||
// 비밀번호: 새로 입력된 경우에만 업데이트
|
||||
if (! empty($validated['smtp_password'])) {
|
||||
$options['smtp']['password'] = encrypt($validated['smtp_password']);
|
||||
} elseif (isset($config?->options['smtp']['password'])) {
|
||||
$options['smtp']['password'] = $config->options['smtp']['password'];
|
||||
}
|
||||
} else {
|
||||
// platform 모드에서는 SMTP 설정 제거
|
||||
unset($options['smtp'], $options['preset']);
|
||||
}
|
||||
|
||||
// 브랜딩
|
||||
$options['branding'] = array_filter([
|
||||
'company_name' => $validated['branding_company_name'] ?? null,
|
||||
'primary_color' => $validated['branding_primary_color'] ?? '#1a56db',
|
||||
'company_address' => $validated['branding_company_address'] ?? null,
|
||||
'company_phone' => $validated['branding_company_phone'] ?? null,
|
||||
'footer_text' => $validated['branding_footer_text'] ?? 'SAM 시스템에서 발송된 메일입니다.',
|
||||
]);
|
||||
|
||||
$data['options'] = $options;
|
||||
$data['updated_by'] = auth()->id();
|
||||
|
||||
if ($config) {
|
||||
$config->update($data);
|
||||
} else {
|
||||
$data['created_by'] = auth()->id();
|
||||
$config = TenantMailConfig::create($data);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'ok' => true,
|
||||
'message' => '메일 설정이 저장되었습니다.',
|
||||
'data' => $config->fresh(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* SMTP 연결 테스트
|
||||
*/
|
||||
public function test(Request $request, int $tenantId): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'host' => 'required|string',
|
||||
'port' => 'required|integer',
|
||||
'encryption' => 'required|string|in:tls,ssl',
|
||||
'username' => 'required|string',
|
||||
'password' => 'required|string',
|
||||
'preset' => 'nullable|string',
|
||||
'send_test_mail' => 'boolean',
|
||||
]);
|
||||
|
||||
$tester = new SmtpConnectionTester;
|
||||
$result = $tester->testViaLaravel(
|
||||
host: $validated['host'],
|
||||
port: $validated['port'],
|
||||
encryption: $validated['encryption'],
|
||||
username: $validated['username'],
|
||||
password: $validated['password'],
|
||||
testRecipient: ($validated['send_test_mail'] ?? false) ? $validated['username'] : null
|
||||
);
|
||||
|
||||
// 테스트 결과를 config에 기록
|
||||
$config = TenantMailConfig::withoutGlobalScopes()
|
||||
->where('tenant_id', $tenantId)
|
||||
->first();
|
||||
|
||||
if ($config) {
|
||||
$config->setOption('connection_test', [
|
||||
'last_tested_at' => now()->toIso8601String(),
|
||||
'last_result' => $result['success'] ? 'success' : 'failed',
|
||||
'response_time_ms' => $result['response_time_ms'],
|
||||
'server_banner' => $result['server_banner'] ?? null,
|
||||
'tested_by' => auth()->user()?->email,
|
||||
]);
|
||||
$config->save();
|
||||
}
|
||||
|
||||
if ($result['success']) {
|
||||
return response()->json([
|
||||
'ok' => true,
|
||||
'message' => $result['message'],
|
||||
'data' => [
|
||||
'response_time_ms' => $result['response_time_ms'],
|
||||
'server_banner' => $result['server_banner'],
|
||||
'test_mail_sent' => $result['test_mail_sent'] ?? false,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'ok' => false,
|
||||
'message' => $result['message'],
|
||||
'data' => [
|
||||
'error_code' => $result['error_code'],
|
||||
'troubleshoot' => SmtpConnectionTester::getTroubleshoot(
|
||||
$result['error_code'] ?? 'UNKNOWN',
|
||||
$validated['preset'] ?? null
|
||||
),
|
||||
'response_time_ms' => $result['response_time_ms'],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* SMTP 프리셋 목록
|
||||
*/
|
||||
public function presets(): JsonResponse
|
||||
{
|
||||
return response()->json([
|
||||
'ok' => true,
|
||||
'data' => config('mail-presets', []),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mail;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Mail\Attachment;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Mail\Mailables\Content;
|
||||
use Illuminate\Mail\Mailables\Envelope;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class PayslipMail extends Mailable
|
||||
{
|
||||
use Queueable, SerializesModels;
|
||||
|
||||
public function __construct(
|
||||
public array $payslipData,
|
||||
private string $pdfContent,
|
||||
private string $fileName,
|
||||
) {}
|
||||
|
||||
public function envelope(): Envelope
|
||||
{
|
||||
$year = $this->payslipData['pay_year'] ?? '';
|
||||
$month = str_pad($this->payslipData['pay_month'] ?? '', 2, '0', STR_PAD_LEFT);
|
||||
|
||||
return new Envelope(
|
||||
from: new \Illuminate\Mail\Mailables\Address('admin@codebridge-x.com', '(주)코드브릿지엑스'),
|
||||
subject: "[SAM] {$year}년{$month}월분 급여명세서",
|
||||
);
|
||||
}
|
||||
|
||||
public function content(): Content
|
||||
{
|
||||
return new Content(
|
||||
view: 'emails.payslip-notification',
|
||||
);
|
||||
}
|
||||
|
||||
public function attachments(): array
|
||||
{
|
||||
return [
|
||||
Attachment::fromData(fn () => $this->pdfContent, $this->fileName)
|
||||
->withMime('application/pdf'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,6 @@
|
||||
*/
|
||||
class AdminApiFlow extends Model
|
||||
{
|
||||
protected $connection = 'codebridge';
|
||||
protected $table = 'admin_api_flows';
|
||||
|
||||
protected $fillable = [
|
||||
|
||||
@@ -37,7 +37,6 @@ class AdminApiFlowRun extends Model
|
||||
|
||||
public const STATUS_PARTIAL = 'PARTIAL';
|
||||
|
||||
protected $connection = 'codebridge';
|
||||
protected $table = 'admin_api_flow_runs';
|
||||
|
||||
public $timestamps = false; // created_at만 사용
|
||||
|
||||
@@ -27,7 +27,6 @@ class AdminPmDailyLog extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
protected $connection = 'codebridge';
|
||||
protected $table = 'admin_pm_daily_logs';
|
||||
|
||||
protected $fillable = [
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
*/
|
||||
class AdminPmDailyLogEntry extends Model
|
||||
{
|
||||
protected $connection = 'codebridge';
|
||||
protected $table = 'admin_pm_daily_log_entries';
|
||||
|
||||
protected $fillable = [
|
||||
|
||||
@@ -29,7 +29,6 @@ class AdminPmIssue extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
protected $connection = 'codebridge';
|
||||
protected $table = 'admin_pm_issues';
|
||||
|
||||
protected $fillable = [
|
||||
|
||||
@@ -28,7 +28,6 @@ class AdminPmProject extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
protected $connection = 'codebridge';
|
||||
protected $table = 'admin_pm_projects';
|
||||
|
||||
protected $fillable = [
|
||||
|
||||
@@ -31,7 +31,6 @@ class AdminPmTask extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
protected $connection = 'codebridge';
|
||||
protected $table = 'admin_pm_tasks';
|
||||
|
||||
protected $fillable = [
|
||||
|
||||
@@ -11,7 +11,6 @@ class AdminRoadmapMilestone extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
protected $connection = 'codebridge';
|
||||
protected $table = 'admin_roadmap_milestones';
|
||||
|
||||
protected $fillable = [
|
||||
|
||||
@@ -12,7 +12,6 @@ class AdminRoadmapPlan extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
protected $connection = 'codebridge';
|
||||
protected $table = 'admin_roadmap_plans';
|
||||
|
||||
protected $fillable = [
|
||||
|
||||
@@ -13,8 +13,6 @@ class Approval extends Model
|
||||
{
|
||||
use BelongsToTenant, SoftDeletes;
|
||||
|
||||
protected $connection = 'mysql';
|
||||
|
||||
protected $table = 'approvals';
|
||||
|
||||
protected $casts = [
|
||||
|
||||
@@ -5,12 +5,9 @@
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class ApprovalStep extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
protected $table = 'approval_steps';
|
||||
|
||||
protected $casts = [
|
||||
@@ -22,7 +19,6 @@ class ApprovalStep extends Model
|
||||
];
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'approval_id',
|
||||
'step_order',
|
||||
'step_type',
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
*/
|
||||
class BizCert extends Model
|
||||
{
|
||||
protected $connection = 'codebridge';
|
||||
protected $table = 'biz_cert';
|
||||
|
||||
protected $fillable = [
|
||||
|
||||
@@ -12,8 +12,6 @@
|
||||
/**
|
||||
* 파일 모델 (Polymorphic)
|
||||
*
|
||||
* @note $connection = 'mysql' 명시: codebridge 모델에서 relation으로 참조 시 connection 상속 방지
|
||||
*
|
||||
* @property int $id
|
||||
* @property int|null $tenant_id
|
||||
* @property int|null $folder_id
|
||||
@@ -33,8 +31,6 @@ class File extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
protected $connection = 'mysql';
|
||||
|
||||
protected $table = 'files';
|
||||
|
||||
protected $fillable = [
|
||||
|
||||
@@ -6,8 +6,6 @@
|
||||
|
||||
class File extends Model
|
||||
{
|
||||
protected $connection = 'mysql';
|
||||
|
||||
protected $table = 'files';
|
||||
|
||||
protected $fillable = [
|
||||
|
||||
@@ -11,8 +11,6 @@ class Department extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
protected $connection = 'mysql';
|
||||
|
||||
protected $table = 'departments';
|
||||
|
||||
protected $fillable = [
|
||||
|
||||
@@ -21,7 +21,6 @@
|
||||
*/
|
||||
class ApiBookmark extends Model
|
||||
{
|
||||
protected $connection = 'codebridge';
|
||||
protected $table = 'admin_api_bookmarks';
|
||||
|
||||
protected $fillable = [
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
*/
|
||||
class ApiDeprecation extends Model
|
||||
{
|
||||
protected $connection = 'codebridge';
|
||||
protected $table = 'admin_api_deprecations';
|
||||
|
||||
protected $fillable = [
|
||||
|
||||
@@ -23,7 +23,6 @@
|
||||
*/
|
||||
class ApiEnvironment extends Model
|
||||
{
|
||||
protected $connection = 'codebridge';
|
||||
protected $table = 'admin_api_environments';
|
||||
|
||||
protected $fillable = [
|
||||
|
||||
@@ -29,7 +29,6 @@ class ApiHistory extends Model
|
||||
*/
|
||||
public const UPDATED_AT = null;
|
||||
|
||||
protected $connection = 'codebridge';
|
||||
protected $table = 'admin_api_histories';
|
||||
|
||||
protected $fillable = [
|
||||
|
||||
@@ -25,7 +25,6 @@
|
||||
*/
|
||||
class ApiTemplate extends Model
|
||||
{
|
||||
protected $connection = 'codebridge';
|
||||
protected $table = 'admin_api_templates';
|
||||
|
||||
protected $fillable = [
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
|
||||
class EsignFieldTemplate extends Model
|
||||
{
|
||||
protected $connection = 'codebridge';
|
||||
protected $table = 'esign_field_templates';
|
||||
|
||||
protected $fillable = [
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
|
||||
class EsignFieldTemplateItem extends Model
|
||||
{
|
||||
protected $connection = 'codebridge';
|
||||
protected $table = 'esign_field_template_items';
|
||||
|
||||
protected $fillable = [
|
||||
|
||||
@@ -14,7 +14,6 @@ class Equipment extends Model
|
||||
{
|
||||
use BelongsToTenant, SoftDeletes;
|
||||
|
||||
protected $connection = 'codebridge';
|
||||
protected $table = 'equipments';
|
||||
|
||||
protected $fillable = [
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
|
||||
class EquipmentInspection extends Model
|
||||
{
|
||||
protected $connection = 'codebridge';
|
||||
use BelongsToTenant;
|
||||
|
||||
protected $fillable = [
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
|
||||
class EquipmentInspectionDetail extends Model
|
||||
{
|
||||
protected $connection = 'codebridge';
|
||||
protected $fillable = [
|
||||
'inspection_id',
|
||||
'template_item_id',
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
|
||||
class EquipmentInspectionTemplate extends Model
|
||||
{
|
||||
protected $connection = 'codebridge';
|
||||
use BelongsToTenant;
|
||||
|
||||
protected $fillable = [
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
|
||||
class EquipmentProcess extends Model
|
||||
{
|
||||
protected $connection = 'codebridge';
|
||||
protected $table = 'equipment_process';
|
||||
|
||||
protected $fillable = [
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
|
||||
class EquipmentRepair extends Model
|
||||
{
|
||||
protected $connection = 'codebridge';
|
||||
use BelongsToTenant, SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
|
||||
@@ -9,7 +9,6 @@ class CondolenceExpense extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
protected $connection = 'codebridge';
|
||||
protected $table = 'condolence_expenses';
|
||||
|
||||
protected $fillable = [
|
||||
|
||||
@@ -9,7 +9,6 @@ class ConsultingFee extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
protected $connection = 'codebridge';
|
||||
protected $table = 'consulting_fees';
|
||||
|
||||
protected $fillable = [
|
||||
|
||||
@@ -9,7 +9,6 @@ class CorporateCard extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
protected $connection = 'codebridge';
|
||||
protected $table = 'corporate_cards';
|
||||
|
||||
protected $fillable = [
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
|
||||
class CorporateCardPrepayment extends Model
|
||||
{
|
||||
protected $connection = 'codebridge';
|
||||
protected $table = 'corporate_card_prepayments';
|
||||
|
||||
protected $fillable = ['tenant_id', 'year_month', 'amount', 'memo', 'items'];
|
||||
|
||||
@@ -9,7 +9,6 @@ class CustomerSettlement extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
protected $connection = 'codebridge';
|
||||
protected $table = 'customer_settlements';
|
||||
|
||||
protected $fillable = [
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
|
||||
class DailyFundMemo extends Model
|
||||
{
|
||||
protected $connection = 'codebridge';
|
||||
protected $table = 'daily_fund_memos';
|
||||
|
||||
protected $fillable = [
|
||||
|
||||
@@ -9,7 +9,6 @@ class DailyFundTransaction extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
protected $connection = 'codebridge';
|
||||
protected $table = 'daily_fund_transactions';
|
||||
|
||||
protected $fillable = [
|
||||
|
||||
@@ -9,7 +9,6 @@ class Income extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
protected $connection = 'codebridge';
|
||||
protected $table = 'incomes';
|
||||
|
||||
protected $fillable = [
|
||||
|
||||
@@ -9,7 +9,6 @@ class SalesRecord extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
protected $connection = 'codebridge';
|
||||
protected $table = 'sales_records';
|
||||
|
||||
protected $fillable = [
|
||||
|
||||
@@ -9,7 +9,6 @@ class VatRecord extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
protected $connection = 'codebridge';
|
||||
protected $table = 'vat_records';
|
||||
|
||||
protected $fillable = [
|
||||
|
||||
@@ -12,7 +12,6 @@ class BusinessIncomePayment extends Model
|
||||
{
|
||||
use ModelTrait, SoftDeletes;
|
||||
|
||||
protected $connection = 'codebridge';
|
||||
protected $table = 'business_income_payments';
|
||||
|
||||
protected $fillable = [
|
||||
|
||||
@@ -111,11 +111,6 @@ public function getResidentNumberAttribute(): ?string
|
||||
return $this->json_extra['resident_number'] ?? null;
|
||||
}
|
||||
|
||||
public function getPersonalEmailAttribute(): ?string
|
||||
{
|
||||
return $this->json_extra['personal_email'] ?? null;
|
||||
}
|
||||
|
||||
public function getBankAccountAttribute(): ?array
|
||||
{
|
||||
return $this->json_extra['bank_account'] ?? null;
|
||||
@@ -174,118 +169,6 @@ public function setJsonExtraValue(string $key, mixed $value): void
|
||||
$this->json_extra = $extra;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 연봉 정보 (salary_info) — 민감 데이터, 별도 접근 제어
|
||||
// =========================================================================
|
||||
|
||||
public function getSalaryInfo(): array
|
||||
{
|
||||
$defaults = [
|
||||
'annual_salary' => null,
|
||||
'effective_date' => null,
|
||||
'notes' => null,
|
||||
'fixed_overtime_hours' => null,
|
||||
'meal_allowance' => 200000,
|
||||
'monthly_salary' => null,
|
||||
'base_salary' => null,
|
||||
'fixed_overtime_pay' => null,
|
||||
'hourly_wage' => null,
|
||||
'monthly_work_hours' => 209,
|
||||
'overtime_multiplier' => 1.5,
|
||||
'history' => [],
|
||||
];
|
||||
|
||||
$data = $this->json_extra['salary_info'] ?? [];
|
||||
|
||||
return array_merge($defaults, $data);
|
||||
}
|
||||
|
||||
public function setSalaryInfo(array $data): void
|
||||
{
|
||||
$current = $this->getSalaryInfo();
|
||||
$history = $current['history'] ?? [];
|
||||
|
||||
// 기존 연봉이 있으면 이력에 추가
|
||||
if ($current['annual_salary'] !== null) {
|
||||
$history[] = [
|
||||
'annual_salary' => $current['annual_salary'],
|
||||
'fixed_overtime_hours' => $current['fixed_overtime_hours'],
|
||||
'meal_allowance' => $current['meal_allowance'],
|
||||
'base_salary' => $current['base_salary'],
|
||||
'fixed_overtime_pay' => $current['fixed_overtime_pay'],
|
||||
'effective_date' => $current['effective_date'],
|
||||
'notes' => $current['notes'],
|
||||
'recorded_at' => now()->format('Y-m-d H:i:s'),
|
||||
'recorded_by' => auth()->user()?->name ?? '-',
|
||||
];
|
||||
}
|
||||
|
||||
$annualSalary = $data['annual_salary'] ?? null;
|
||||
$mealAllowance = $data['meal_allowance'] ?? 200000;
|
||||
$fixedOvertimeHours = $data['fixed_overtime_hours'] ?? null;
|
||||
$breakdown = $this->calculateSalaryBreakdown($annualSalary, $mealAllowance, $fixedOvertimeHours);
|
||||
|
||||
$this->setJsonExtraValue('salary_info', array_merge([
|
||||
'annual_salary' => $annualSalary,
|
||||
'effective_date' => $data['effective_date'] ?? null,
|
||||
'notes' => $data['notes'] ?? null,
|
||||
'fixed_overtime_hours' => $fixedOvertimeHours,
|
||||
'meal_allowance' => $mealAllowance,
|
||||
], $breakdown, ['history' => $history]));
|
||||
}
|
||||
|
||||
/**
|
||||
* 급여 산정 계산
|
||||
*
|
||||
* 공식: (기본급 + 식대) = 월급여 × 209 / (209 + 고정연장근로시간 × 1.5)
|
||||
*/
|
||||
private function calculateSalaryBreakdown(?int $annualSalary, int $mealAllowance, ?int $fixedOvertimeHours): array
|
||||
{
|
||||
$monthlyWorkHours = 209;
|
||||
$overtimeMultiplier = 1.5;
|
||||
|
||||
if (! $annualSalary) {
|
||||
return [
|
||||
'monthly_salary' => null,
|
||||
'base_salary' => null,
|
||||
'fixed_overtime_pay' => null,
|
||||
'hourly_wage' => null,
|
||||
'monthly_work_hours' => $monthlyWorkHours,
|
||||
'overtime_multiplier' => $overtimeMultiplier,
|
||||
];
|
||||
}
|
||||
|
||||
$monthlySalary = (int) round($annualSalary / 12);
|
||||
$otFactor = ($fixedOvertimeHours ?? 0) * $overtimeMultiplier;
|
||||
$basePlusMeal = (int) round($monthlySalary * $monthlyWorkHours / ($monthlyWorkHours + $otFactor));
|
||||
$baseSalary = $basePlusMeal - $mealAllowance;
|
||||
$hourlyWage = (int) floor($basePlusMeal / $monthlyWorkHours);
|
||||
$fixedOvertimePay = $monthlySalary - $baseSalary - $mealAllowance;
|
||||
|
||||
return [
|
||||
'monthly_salary' => $monthlySalary,
|
||||
'base_salary' => $baseSalary,
|
||||
'fixed_overtime_pay' => $fixedOvertimePay,
|
||||
'hourly_wage' => $hourlyWage,
|
||||
'monthly_work_hours' => $monthlyWorkHours,
|
||||
'overtime_multiplier' => $overtimeMultiplier,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* toArray 시 salary_info 제거 (일반 API 응답에서 연봉 정보 노출 방지)
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
$array = parent::toArray();
|
||||
|
||||
if (isset($array['json_extra']['salary_info'])) {
|
||||
unset($array['json_extra']['salary_info']);
|
||||
}
|
||||
|
||||
return $array;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 스코프
|
||||
// =========================================================================
|
||||
|
||||
@@ -39,7 +39,6 @@ class Payroll extends Model
|
||||
'paid_at',
|
||||
'withdrawal_id',
|
||||
'note',
|
||||
'options',
|
||||
'created_by',
|
||||
'updated_by',
|
||||
'deleted_by',
|
||||
@@ -66,7 +65,6 @@ class Payroll extends Model
|
||||
'deductions' => 'array',
|
||||
'confirmed_at' => 'datetime',
|
||||
'paid_at' => 'datetime',
|
||||
'options' => 'array',
|
||||
];
|
||||
|
||||
protected $attributes = [
|
||||
@@ -133,12 +131,8 @@ public function getPeriodLabelAttribute(): string
|
||||
// 상태 헬퍼
|
||||
// =========================================================================
|
||||
|
||||
public function isEditable(?bool $isSuperAdmin = null): bool
|
||||
public function isEditable(): bool
|
||||
{
|
||||
if ($isSuperAdmin && in_array($this->status, [self::STATUS_CONFIRMED, self::STATUS_PAID])) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $this->status === self::STATUS_DRAFT;
|
||||
}
|
||||
|
||||
@@ -162,11 +156,6 @@ public function isDeletable(): bool
|
||||
return $this->status === self::STATUS_DRAFT;
|
||||
}
|
||||
|
||||
public function isUnpayable(): bool
|
||||
{
|
||||
return $this->status === self::STATUS_PAID;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 스코프
|
||||
// =========================================================================
|
||||
|
||||
@@ -12,7 +12,6 @@ class InterviewKnowledge extends Model
|
||||
{
|
||||
use BelongsToTenant, ModelTrait, SoftDeletes;
|
||||
|
||||
protected $connection = 'codebridge';
|
||||
protected $table = 'interview_knowledge';
|
||||
|
||||
protected $fillable = [
|
||||
|
||||
@@ -12,8 +12,6 @@ class InterviewProject extends Model
|
||||
{
|
||||
use BelongsToTenant, ModelTrait, SoftDeletes;
|
||||
|
||||
protected $connection = 'mysql';
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'company_name',
|
||||
|
||||
@@ -13,7 +13,6 @@ class ConstructionSitePhoto extends Model
|
||||
{
|
||||
use BelongsToTenant, SoftDeletes;
|
||||
|
||||
protected $connection = 'codebridge';
|
||||
protected $table = 'construction_site_photos';
|
||||
|
||||
protected $fillable = [
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
|
||||
class ConstructionSitePhotoRow extends Model
|
||||
{
|
||||
protected $connection = 'codebridge';
|
||||
protected $table = 'construction_site_photo_rows';
|
||||
|
||||
protected $fillable = [
|
||||
|
||||
@@ -13,7 +13,6 @@ class MeetingMinute extends Model
|
||||
{
|
||||
use BelongsToTenant, SoftDeletes;
|
||||
|
||||
protected $connection = 'codebridge';
|
||||
protected $table = 'meeting_minutes';
|
||||
|
||||
const STATUS_DRAFT = 'DRAFT';
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
|
||||
class MeetingMinuteSegment extends Model
|
||||
{
|
||||
protected $connection = 'codebridge';
|
||||
protected $table = 'meeting_minute_segments';
|
||||
|
||||
protected $fillable = [
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Juil;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class PmisAttendanceEquipment extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
protected $table = 'pmis_attendance_equipments';
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'attendance_id',
|
||||
'equipment_name',
|
||||
'specification',
|
||||
'equipment_number',
|
||||
'operator',
|
||||
'man_days',
|
||||
'work_content',
|
||||
'sort_order',
|
||||
'options',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'man_days' => 'decimal:1',
|
||||
'options' => 'array',
|
||||
];
|
||||
|
||||
public function attendance(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(PmisDailyAttendance::class, 'attendance_id');
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Juil;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class PmisAttendanceWorker extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
protected $table = 'pmis_attendance_workers';
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'attendance_id',
|
||||
'work_type',
|
||||
'job_type',
|
||||
'name',
|
||||
'man_days',
|
||||
'amount',
|
||||
'work_content',
|
||||
'sort_order',
|
||||
'options',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'man_days' => 'decimal:1',
|
||||
'amount' => 'integer',
|
||||
'options' => 'array',
|
||||
];
|
||||
|
||||
public function attendance(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(PmisDailyAttendance::class, 'attendance_id');
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Juil;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class PmisConstructionWorker extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
protected $table = 'pmis_construction_workers';
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'company_name',
|
||||
'trade_name',
|
||||
'job_type_id',
|
||||
'name',
|
||||
'phone',
|
||||
'birth_date',
|
||||
'ssn_gender',
|
||||
'wage',
|
||||
'blood_type',
|
||||
'remark',
|
||||
'options',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'wage' => 'integer',
|
||||
'options' => 'array',
|
||||
];
|
||||
|
||||
public function jobType(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(PmisJobType::class, 'job_type_id');
|
||||
}
|
||||
|
||||
public function scopeTenant($query, $tenantId)
|
||||
{
|
||||
return $query->where('tenant_id', $tenantId);
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Juil;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class PmisDailyAttendance extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
protected $table = 'pmis_daily_attendances';
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'date',
|
||||
'company_name',
|
||||
'weather',
|
||||
'status',
|
||||
'notes',
|
||||
'options',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'date' => 'date',
|
||||
'options' => 'array',
|
||||
];
|
||||
|
||||
public function scopeTenant($query, $tenantId)
|
||||
{
|
||||
return $query->where('tenant_id', $tenantId);
|
||||
}
|
||||
|
||||
public function workers(): HasMany
|
||||
{
|
||||
return $this->hasMany(PmisAttendanceWorker::class, 'attendance_id')->orderBy('sort_order');
|
||||
}
|
||||
|
||||
public function equipments(): HasMany
|
||||
{
|
||||
return $this->hasMany(PmisAttendanceEquipment::class, 'attendance_id')->orderBy('sort_order');
|
||||
}
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Juil;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class PmisDailyWorkReport extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
protected $table = 'pmis_daily_work_reports';
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'date',
|
||||
'company_name',
|
||||
'weather',
|
||||
'temp_low',
|
||||
'temp_high',
|
||||
'precipitation',
|
||||
'snowfall',
|
||||
'fine_dust',
|
||||
'ultra_fine_dust',
|
||||
'work_content_today',
|
||||
'work_content_tomorrow',
|
||||
'notes',
|
||||
'status',
|
||||
'options',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'date' => 'date',
|
||||
'temp_low' => 'decimal:1',
|
||||
'temp_high' => 'decimal:1',
|
||||
'precipitation' => 'decimal:1',
|
||||
'snowfall' => 'decimal:1',
|
||||
'options' => 'array',
|
||||
];
|
||||
|
||||
public function scopeTenant($query, $tenantId)
|
||||
{
|
||||
return $query->where('tenant_id', $tenantId);
|
||||
}
|
||||
|
||||
public function workers(): HasMany
|
||||
{
|
||||
return $this->hasMany(PmisWorkReportWorker::class, 'report_id')->orderBy('sort_order');
|
||||
}
|
||||
|
||||
public function equipments(): HasMany
|
||||
{
|
||||
return $this->hasMany(PmisWorkReportEquipment::class, 'report_id')->orderBy('sort_order');
|
||||
}
|
||||
|
||||
public function materials(): HasMany
|
||||
{
|
||||
return $this->hasMany(PmisWorkReportMaterial::class, 'report_id')->orderBy('sort_order');
|
||||
}
|
||||
|
||||
public function volumes(): HasMany
|
||||
{
|
||||
return $this->hasMany(PmisWorkReportVolume::class, 'report_id')->orderBy('sort_order');
|
||||
}
|
||||
|
||||
public function photos(): HasMany
|
||||
{
|
||||
return $this->hasMany(PmisWorkReportPhoto::class, 'report_id')->orderBy('sort_order');
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Juil;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class PmisEquipment extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
protected $table = 'pmis_equipments';
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'company_name',
|
||||
'equipment_code',
|
||||
'equipment_name',
|
||||
'specification',
|
||||
'unit',
|
||||
'equipment_number',
|
||||
'operator',
|
||||
'inspection_end_date',
|
||||
'inspection_not_applicable',
|
||||
'insurance_end_date',
|
||||
'insurance_not_applicable',
|
||||
'options',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'inspection_end_date' => 'date',
|
||||
'inspection_not_applicable' => 'boolean',
|
||||
'insurance_end_date' => 'date',
|
||||
'insurance_not_applicable' => 'boolean',
|
||||
'options' => 'array',
|
||||
];
|
||||
|
||||
public function scopeTenant($query, $tenantId)
|
||||
{
|
||||
return $query->where('tenant_id', $tenantId);
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Juil;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class PmisJobType extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
protected $table = 'pmis_job_types';
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'name',
|
||||
'sort_order',
|
||||
'is_active',
|
||||
'options',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'is_active' => 'boolean',
|
||||
'options' => 'array',
|
||||
];
|
||||
|
||||
public function scopeTenant($query, $tenantId)
|
||||
{
|
||||
return $query->where('tenant_id', $tenantId);
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Juil;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class PmisMaterial extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
protected $table = 'pmis_materials';
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'company_name',
|
||||
'material_code',
|
||||
'material_name',
|
||||
'specification',
|
||||
'unit',
|
||||
'design_quantity',
|
||||
'options',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'design_quantity' => 'decimal:2',
|
||||
'options' => 'array',
|
||||
];
|
||||
|
||||
public function scopeTenant($query, $tenantId)
|
||||
{
|
||||
return $query->where('tenant_id', $tenantId);
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Juil;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class PmisWorkReportEquipment extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
protected $table = 'pmis_work_report_equipments';
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'report_id',
|
||||
'equipment_name',
|
||||
'specification',
|
||||
'prev_cumulative',
|
||||
'today_count',
|
||||
'sort_order',
|
||||
'options',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'prev_cumulative' => 'integer',
|
||||
'today_count' => 'integer',
|
||||
'options' => 'array',
|
||||
];
|
||||
|
||||
public function report(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(PmisDailyWorkReport::class, 'report_id');
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Juil;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class PmisWorkReportMaterial extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
protected $table = 'pmis_work_report_materials';
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'report_id',
|
||||
'material_name',
|
||||
'specification',
|
||||
'unit',
|
||||
'design_qty',
|
||||
'prev_cumulative',
|
||||
'today_count',
|
||||
'sort_order',
|
||||
'options',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'design_qty' => 'decimal:2',
|
||||
'prev_cumulative' => 'decimal:2',
|
||||
'today_count' => 'decimal:2',
|
||||
'options' => 'array',
|
||||
];
|
||||
|
||||
public function report(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(PmisDailyWorkReport::class, 'report_id');
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Juil;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class PmisWorkReportPhoto extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
protected $table = 'pmis_work_report_photos';
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'report_id',
|
||||
'photo_path',
|
||||
'location',
|
||||
'content',
|
||||
'photo_date',
|
||||
'sort_order',
|
||||
'options',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'photo_date' => 'date',
|
||||
'options' => 'array',
|
||||
];
|
||||
|
||||
public function report(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(PmisDailyWorkReport::class, 'report_id');
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Juil;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class PmisWorkReportVolume extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
protected $table = 'pmis_work_report_volumes';
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'report_id',
|
||||
'work_type',
|
||||
'sub_work_type',
|
||||
'unit',
|
||||
'design_qty',
|
||||
'prev_cumulative',
|
||||
'today_count',
|
||||
'sort_order',
|
||||
'options',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'design_qty' => 'decimal:2',
|
||||
'prev_cumulative' => 'decimal:2',
|
||||
'today_count' => 'decimal:2',
|
||||
'options' => 'array',
|
||||
];
|
||||
|
||||
public function report(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(PmisDailyWorkReport::class, 'report_id');
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Juil;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class PmisWorkReportWorker extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
protected $table = 'pmis_work_report_workers';
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'report_id',
|
||||
'work_type',
|
||||
'job_type',
|
||||
'prev_cumulative',
|
||||
'today_count',
|
||||
'sort_order',
|
||||
'options',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'prev_cumulative' => 'integer',
|
||||
'today_count' => 'integer',
|
||||
'options' => 'array',
|
||||
];
|
||||
|
||||
public function report(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(PmisDailyWorkReport::class, 'report_id');
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Juil;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class PmisWorkVolume extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
protected $table = 'pmis_work_volumes';
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'work_type',
|
||||
'sub_work_type',
|
||||
'unit',
|
||||
'design_quantity',
|
||||
'daily_report_applied',
|
||||
'options',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'design_quantity' => 'decimal:2',
|
||||
'daily_report_applied' => 'boolean',
|
||||
'options' => 'array',
|
||||
];
|
||||
|
||||
public function scopeTenant($query, $tenantId)
|
||||
{
|
||||
return $query->where('tenant_id', $tenantId);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user