Compare commits
187 Commits
b6d2b9942e
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0ca3242e83 | ||
| 5448f0e57d | |||
|
|
4e19e3ed67 | ||
|
|
88d9192618 | ||
|
|
7b93fd7f68 | ||
|
|
cefad468b9 | ||
|
|
e371b7c9ab | ||
|
|
9ad76ceb82 | ||
|
|
f231c60d3d | ||
|
|
76e098337f | ||
| 57d8b97dde | |||
| f5b60aab38 | |||
| f3849808d5 | |||
|
|
6c208cfb2c | ||
|
|
2d32faa9b5 | ||
|
|
c4b4be495a | ||
|
|
723b5a8e1a | ||
|
|
8c301b54e3 | ||
|
|
19c524d692 | ||
|
|
964ee40e8d | ||
|
|
f401e17447 | ||
|
|
069d0206a0 | ||
| 8c16993746 | |||
| 3a889b33ef | |||
| 073ad11ecd | |||
| 479059747b | |||
| 12373edf8c | |||
| 3bae303447 | |||
| b55cbc2ec4 | |||
| f0b1b5e33a | |||
|
|
918ae0ebc1 | ||
|
|
2c9e5ae2da | ||
|
|
eeda6d980e | ||
|
|
82621a6045 | ||
|
|
18a6f3e7aa | ||
|
|
5828261dce | ||
|
|
0ab3d5ab88 | ||
|
|
7eab19508a | ||
|
|
100a78b6e5 | ||
|
|
0be88f95ca | ||
|
|
3fd412f89d | ||
|
|
f662a389f7 | ||
|
|
6f48b86206 | ||
|
|
8222ba667f | ||
|
|
f0464d4f8c | ||
| bbaeefb6b5 | |||
| 047524c19f | |||
| 079f4b0ffb | |||
| e061faadc2 | |||
| 30c2484440 | |||
| 334e39d2de | |||
| e372b9543b | |||
|
|
4e192d1c00 | ||
|
|
6d1925fcd1 | ||
| bd500a87bd | |||
| c46b950fde | |||
| 22f72f1bbc | |||
|
|
6f0ad1cf2d | ||
|
|
7ff9206f93 | ||
|
|
d8f2361c88 | ||
| 28ae481a7d | |||
| 74e3c21ee0 | |||
| 45a207d4a8 | |||
| 3fc5f511bc | |||
|
|
ee9f4d0b8f | ||
|
|
ca259ccb18 | ||
|
|
3929c5fd1e | ||
|
|
56c60ec3df | ||
|
|
60c4256bd0 | ||
|
|
1861f4daf2 | ||
|
|
c62e59ad17 | ||
|
|
e6f13e3870 | ||
|
|
1d5d161e05 | ||
|
|
0044779eb4 | ||
| 3ac64d5b76 | |||
| 2231c9a48f | |||
| ff8553055c | |||
| f2eede6e3a | |||
| c5d5b5d076 | |||
| 5ebf940873 | |||
| 293330c418 | |||
| a845f52fc0 | |||
| 0f26ea546a | |||
| 3600c7b12b | |||
| a6e29bc1f3 | |||
| 0aa0a8592d | |||
| 38c2402771 | |||
| 59d13eeb9f | |||
| 2df8ecf765 | |||
|
|
ad93743bdc | ||
|
|
449fce1d2b | ||
|
|
9d4143a4dc | ||
|
|
c5a0115e01 | ||
|
|
eb28b577e0 | ||
|
|
22160e5904 | ||
|
|
0ea5fa5eb9 | ||
|
|
58fedb0d43 | ||
|
|
56e7164243 | ||
|
|
a67c5d9fca | ||
|
|
816c25a631 | ||
|
|
12d172e4c3 | ||
|
|
be9c1baa34 | ||
|
|
a7973bb555 | ||
|
|
96def0d71e | ||
|
|
846ced3ead | ||
|
|
0f25a5d4e1 | ||
|
|
c57e768b87 | ||
|
|
7fe856f3b7 | ||
|
|
652ac3d1ec | ||
|
|
03f86f375e | ||
|
|
31d2f08dd8 | ||
|
|
8c9f2fcfb5 | ||
|
|
f41605ca73 | ||
|
|
66d1004bc2 | ||
|
|
ce1f91074e | ||
|
|
558a393c85 | ||
|
|
ac72487eff | ||
| 3d4dd9f252 | |||
| 2507dcf142 | |||
| 9b8cdfa2a5 | |||
| 7432fb16aa | |||
| d4f21f06d6 | |||
| 1f7f45ee60 | |||
| cd847e01a0 | |||
| ef7d9fae24 | |||
| 1a8bb46137 | |||
| 897511cb55 | |||
| 5ee97c2d74 | |||
| 8518621432 | |||
| fc537898fc | |||
|
|
fefd129795 | ||
|
|
1b2363d661 | ||
|
|
f1a3e0f164 | ||
|
|
e8da2ea1b1 | ||
|
|
e0bb19a017 | ||
|
|
74a60e06bc | ||
|
|
2f3ec13b24 | ||
|
|
94b96e22f6 | ||
|
|
a173a5a4fc | ||
|
|
66da2972fa | ||
|
|
282bf26eec | ||
|
|
b86af29cc9 | ||
|
|
f665d3aea8 | ||
|
|
e637e3d1f7 | ||
|
|
7cf70dbcaa | ||
|
|
76192fc177 | ||
|
|
da04b84bb5 | ||
|
|
1deeafc4de | ||
|
|
4f3467c3b0 | ||
|
|
e9fd75fa74 | ||
|
|
23c6cf6919 | ||
|
|
42443349dc | ||
|
|
ad27090bfc | ||
|
|
b7465becab | ||
|
|
83a774572a | ||
|
|
da1142af62 | ||
|
|
b3c7d08b2c | ||
| 7e309e4057 | |||
|
|
f79d008777 | ||
|
|
abe04607e4 | ||
|
|
3ca161e9e2 | ||
|
|
587bdf5d80 | ||
|
|
f2d36c3616 | ||
|
|
397d50de1f | ||
|
|
73949b1282 | ||
|
|
e40555ad37 | ||
|
|
79da7a6da7 | ||
|
|
b04d407fdb | ||
|
|
c32d68f069 | ||
| afc1aa72a8 | |||
| f970b6bf4b | |||
| e6c02292d2 | |||
| 9e84fa04a6 | |||
|
|
ff8b37670e | ||
|
|
75408d925f | ||
|
|
dd11f780b4 | ||
|
|
c94bef1dae | ||
| f53f04de65 | |||
|
|
96c4f71607 | ||
|
|
0d1d056b13 | ||
| ac7279606d | |||
|
|
fcb377a40c | ||
|
|
1a2350db7d | ||
|
|
0ff731ab09 | ||
| 0345ddcce3 | |||
|
|
a6e547f40d | ||
|
|
e819635ea6 |
@@ -54,4 +54,4 @@ ## 관련 파일
|
||||
|
||||
- `api/app/Services/ComprehensiveAnalysisService.php`
|
||||
- `api/database/seeders/ComprehensiveAnalysisSeeder.php`
|
||||
- `docs/plans/react-mock-remaining-tasks.md`
|
||||
- `docs/dev/dev_plans/react-mock-remaining-tasks.md`
|
||||
|
||||
@@ -15,7 +15,7 @@ ## Phase 구성
|
||||
- Phase 5: MNG 관리자 패널 (4항목) — mng/ /system/alerts
|
||||
|
||||
## 핵심 파일
|
||||
- 계획 문서: docs/plans/db-backup-system-plan.md
|
||||
- 계획 문서: docs/dev/dev_plans/db-backup-system-plan.md
|
||||
- 개발서버: 114.203.209.83 (SSH: hskwon)
|
||||
- DB: sam (메인) + sam_stat (통계)
|
||||
- Slack 웹훅: api/.env → LOG_SLACK_WEBHOOK_URL (이미 설정됨)
|
||||
|
||||
@@ -16,7 +16,7 @@ ### 생성된 파일
|
||||
| 파일 | 설명 |
|
||||
|------|------|
|
||||
| `app/Http/Requests/Quote/QuoteBomBulkCalculateRequest.php` | 다건 BOM 산출 FormRequest |
|
||||
| `api/docs/changes/20260102_1300_quote_bom_bulk_calculation.md` | 변경 내용 문서 |
|
||||
| `api/docs/dev/changes/20260102_1300_quote_bom_bulk_calculation.md` | 변경 내용 문서 |
|
||||
|
||||
### 수정된 파일
|
||||
| 파일 | 설명 |
|
||||
@@ -93,9 +93,9 @@ ### QuoteCalculationService::calculateBomBulk()
|
||||
- 개별 품목 실패가 전체에 영향 없음 (예외 처리)
|
||||
|
||||
## 관련 문서
|
||||
- 계획 문서: `docs/plans/quote-calculation-api-plan.md`
|
||||
- Phase 1.1 문서: `docs/changes/20260102_quote_bom_calculation_api.md`
|
||||
- Phase 1.2 문서: `docs/changes/20260102_1300_quote_bom_bulk_calculation.md`
|
||||
- 계획 문서: `docs/dev/dev_plans/quote-calculation-api-plan.md`
|
||||
- Phase 1.1 문서: `docs/dev/changes/20260102_quote_bom_calculation_api.md`
|
||||
- Phase 1.2 문서: `docs/dev/changes/20260102_1300_quote_bom_bulk_calculation.md`
|
||||
|
||||
## 다음 단계
|
||||
- React 프론트엔드에서 `/calculate/bom/bulk` API 연동
|
||||
|
||||
@@ -107,3 +107,19 @@ fixed_tools: []
|
||||
# override of the corresponding setting in serena_config.yml, see the documentation there.
|
||||
# If null or missing, the value from the global config is used.
|
||||
symbol_info_budget:
|
||||
|
||||
# The language backend to use for this project.
|
||||
# If not set, the global setting from serena_config.yml is used.
|
||||
# Valid values: LSP, JetBrains
|
||||
# Note: the backend is fixed at startup. If a project with a different backend
|
||||
# is activated post-init, an error will be returned.
|
||||
language_backend:
|
||||
|
||||
# line ending convention to use when writing source files.
|
||||
# Possible values: unset (use global setting), "lf", "crlf", or "native" (platform default)
|
||||
# This does not affect Serena's own files (e.g. memories and configuration files), which always use native line endings.
|
||||
line_ending:
|
||||
|
||||
# list of regex patterns which, when matched, mark a memory entry as read‑only.
|
||||
# Extends the list from the global configuration, merging the two lists.
|
||||
read_only_memory_patterns: []
|
||||
|
||||
130
Jenkinsfile
vendored
130
Jenkinsfile
vendored
@@ -1,6 +1,12 @@
|
||||
pipeline {
|
||||
agent any
|
||||
|
||||
parameters {
|
||||
choice(name: 'ACTION', choices: ['deploy', 'rollback'], description: '배포 또는 롤백')
|
||||
choice(name: 'ROLLBACK_TARGET', choices: ['production', 'stage'], description: '롤백 대상 환경')
|
||||
string(name: 'ROLLBACK_RELEASE', defaultValue: '', description: '롤백할 릴리스 ID (예: 20260310_120000). 비워두면 직전 릴리스로 롤백')
|
||||
}
|
||||
|
||||
options {
|
||||
disableConcurrentBuilds()
|
||||
}
|
||||
@@ -8,10 +14,73 @@ pipeline {
|
||||
environment {
|
||||
DEPLOY_USER = 'hskwon'
|
||||
RELEASE_ID = new Date().format('yyyyMMdd_HHmmss')
|
||||
PROD_SERVER = '211.117.60.189'
|
||||
}
|
||||
|
||||
stages {
|
||||
|
||||
// ── 롤백: 릴리스 목록 조회 ──
|
||||
stage('Rollback: List Releases') {
|
||||
when { expression { params.ACTION == 'rollback' } }
|
||||
steps {
|
||||
script {
|
||||
def basePath = params.ROLLBACK_TARGET == 'production' ? '/home/webservice/api' : '/home/webservice/api-stage'
|
||||
sshagent(credentials: ['deploy-ssh-key']) {
|
||||
def releases = sh(script: "ssh ${DEPLOY_USER}@${PROD_SERVER} 'ls -1dt ${basePath}/releases/*/ | head -6 | xargs -I{} basename {}'", returnStdout: true).trim()
|
||||
def current = sh(script: "ssh ${DEPLOY_USER}@${PROD_SERVER} 'basename \$(readlink -f ${basePath}/current)'", returnStdout: true).trim()
|
||||
echo "=== ${params.ROLLBACK_TARGET} 릴리스 목록 ==="
|
||||
echo "현재 활성: ${current}"
|
||||
echo "사용 가능:\n${releases}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── 롤백: symlink 전환 ──
|
||||
stage('Rollback: Switch Release') {
|
||||
when { expression { params.ACTION == 'rollback' } }
|
||||
steps {
|
||||
script {
|
||||
def basePath = params.ROLLBACK_TARGET == 'production' ? '/home/webservice/api' : '/home/webservice/api-stage'
|
||||
|
||||
sshagent(credentials: ['deploy-ssh-key']) {
|
||||
def targetRelease = params.ROLLBACK_RELEASE
|
||||
if (!targetRelease?.trim()) {
|
||||
// 비워두면 직전 릴리스로 롤백
|
||||
targetRelease = sh(script: "ssh ${DEPLOY_USER}@${PROD_SERVER} 'ls -1dt ${basePath}/releases/*/ | sed -n 2p | xargs basename'", returnStdout: true).trim()
|
||||
}
|
||||
|
||||
// 릴리스 존재 여부 확인
|
||||
sh "ssh ${DEPLOY_USER}@${PROD_SERVER} 'test -d ${basePath}/releases/${targetRelease}'"
|
||||
|
||||
slackSend channel: '#deploy_api', color: '#FF9800', tokenCredentialId: 'slack-token',
|
||||
message: "🔄 *api* ${params.ROLLBACK_TARGET} 롤백 시작 → ${targetRelease}\n<${env.BUILD_URL}|빌드 #${env.BUILD_NUMBER}>"
|
||||
|
||||
sh """
|
||||
ssh ${DEPLOY_USER}@${PROD_SERVER} '
|
||||
ln -sfn ${basePath}/releases/${targetRelease} ${basePath}/current &&
|
||||
cd ${basePath}/current &&
|
||||
php artisan config:cache &&
|
||||
php artisan route:cache &&
|
||||
php artisan view:cache &&
|
||||
sudo systemctl reload php8.4-fpm
|
||||
'
|
||||
"""
|
||||
|
||||
if (params.ROLLBACK_TARGET == 'production') {
|
||||
sh "ssh ${DEPLOY_USER}@${PROD_SERVER} 'sudo supervisorctl restart sam-queue-worker:*'"
|
||||
}
|
||||
|
||||
slackSend channel: '#deploy_api', color: 'good', tokenCredentialId: 'slack-token',
|
||||
message: "✅ *api* ${params.ROLLBACK_TARGET} 롤백 완료 → ${targetRelease}\n<${env.BUILD_URL}|빌드 #${env.BUILD_NUMBER}>"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── 일반 배포: Checkout ──
|
||||
stage('Checkout') {
|
||||
when { expression { params.ACTION == 'deploy' } }
|
||||
steps {
|
||||
checkout scm
|
||||
script {
|
||||
@@ -24,17 +93,22 @@ pipeline {
|
||||
|
||||
// ── main → 운영서버 Stage 배포 ──
|
||||
stage('Deploy Stage') {
|
||||
when { branch 'main' }
|
||||
when {
|
||||
allOf {
|
||||
branch 'main'
|
||||
expression { params.ACTION == 'deploy' }
|
||||
}
|
||||
}
|
||||
steps {
|
||||
sshagent(credentials: ['deploy-ssh-key']) {
|
||||
sh """
|
||||
ssh ${DEPLOY_USER}@211.117.60.189 'mkdir -p /home/webservice/api-stage/releases/${RELEASE_ID}'
|
||||
ssh ${DEPLOY_USER}@${PROD_SERVER} 'mkdir -p /home/webservice/api-stage/releases/${RELEASE_ID}'
|
||||
rsync -az --delete \
|
||||
--exclude='.git' --exclude='.env' \
|
||||
--exclude='storage/app' --exclude='storage/logs' \
|
||||
--exclude='storage/framework/sessions' --exclude='storage/framework/cache' \
|
||||
. ${DEPLOY_USER}@211.117.60.189:/home/webservice/api-stage/releases/${RELEASE_ID}/
|
||||
ssh ${DEPLOY_USER}@211.117.60.189 '
|
||||
. ${DEPLOY_USER}@${PROD_SERVER}:/home/webservice/api-stage/releases/${RELEASE_ID}/
|
||||
ssh ${DEPLOY_USER}@${PROD_SERVER} '
|
||||
cd /home/webservice/api-stage/releases/${RELEASE_ID} &&
|
||||
mkdir -p bootstrap/cache storage/framework/{views,cache/data,sessions} storage/logs &&
|
||||
sudo chown -R www-data:webservice storage bootstrap/cache &&
|
||||
@@ -71,17 +145,22 @@ pipeline {
|
||||
|
||||
// ── main → 운영서버 Production 배포 ──
|
||||
stage('Deploy Production') {
|
||||
when { branch 'main' }
|
||||
when {
|
||||
allOf {
|
||||
branch 'main'
|
||||
expression { params.ACTION == 'deploy' }
|
||||
}
|
||||
}
|
||||
steps {
|
||||
sshagent(credentials: ['deploy-ssh-key']) {
|
||||
sh """
|
||||
ssh ${DEPLOY_USER}@211.117.60.189 'mkdir -p /home/webservice/api/releases/${RELEASE_ID}'
|
||||
ssh ${DEPLOY_USER}@${PROD_SERVER} 'mkdir -p /home/webservice/api/releases/${RELEASE_ID}'
|
||||
rsync -az --delete \
|
||||
--exclude='.git' --exclude='.env' \
|
||||
--exclude='storage/app' --exclude='storage/logs' \
|
||||
--exclude='storage/framework/sessions' --exclude='storage/framework/cache' \
|
||||
. ${DEPLOY_USER}@211.117.60.189:/home/webservice/api/releases/${RELEASE_ID}/
|
||||
ssh ${DEPLOY_USER}@211.117.60.189 '
|
||||
. ${DEPLOY_USER}@${PROD_SERVER}:/home/webservice/api/releases/${RELEASE_ID}/
|
||||
ssh ${DEPLOY_USER}@${PROD_SERVER} '
|
||||
cd /home/webservice/api/releases/${RELEASE_ID} &&
|
||||
mkdir -p bootstrap/cache storage/framework/{views,cache/data,sessions} storage/logs &&
|
||||
sudo chown -R www-data:webservice storage bootstrap/cache &&
|
||||
@@ -109,23 +188,32 @@ pipeline {
|
||||
|
||||
post {
|
||||
success {
|
||||
slackSend channel: '#deploy_api', color: 'good', tokenCredentialId: 'slack-token',
|
||||
message: "✅ *api* 배포 성공 (`${env.BRANCH_NAME}`)\n${env.GIT_COMMIT_MSG}\n<${env.BUILD_URL}|빌드 #${env.BUILD_NUMBER}>"
|
||||
script {
|
||||
if (params.ACTION == 'deploy') {
|
||||
slackSend channel: '#deploy_api', color: 'good', tokenCredentialId: 'slack-token',
|
||||
message: "✅ *api* 배포 성공 (`${env.BRANCH_NAME}`)\n${env.GIT_COMMIT_MSG}\n<${env.BUILD_URL}|빌드 #${env.BUILD_NUMBER}>"
|
||||
}
|
||||
}
|
||||
}
|
||||
failure {
|
||||
slackSend channel: '#deploy_api', color: 'danger', tokenCredentialId: 'slack-token',
|
||||
message: "❌ *api* 배포 실패 (`${env.BRANCH_NAME}`)\n${env.GIT_COMMIT_MSG}\n<${env.BUILD_URL}|빌드 #${env.BUILD_NUMBER}>"
|
||||
script {
|
||||
if (env.BRANCH_NAME == 'main') {
|
||||
sshagent(credentials: ['deploy-ssh-key']) {
|
||||
sh """
|
||||
ssh ${DEPLOY_USER}@211.117.60.189 '
|
||||
PREV=\$(ls -1dt /home/webservice/api/releases/*/ | sed -n "2p" | xargs basename 2>/dev/null) &&
|
||||
[ -n "\$PREV" ] && ln -sfn /home/webservice/api/releases/\$PREV /home/webservice/api/current &&
|
||||
sudo systemctl reload php8.4-fpm
|
||||
' || true
|
||||
"""
|
||||
if (params.ACTION == 'deploy') {
|
||||
slackSend channel: '#deploy_api', color: 'danger', tokenCredentialId: 'slack-token',
|
||||
message: "❌ *api* 배포 실패 (`${env.BRANCH_NAME}`)\n${env.GIT_COMMIT_MSG}\n<${env.BUILD_URL}|빌드 #${env.BUILD_NUMBER}>"
|
||||
if (env.BRANCH_NAME == 'main') {
|
||||
sshagent(credentials: ['deploy-ssh-key']) {
|
||||
sh """
|
||||
ssh ${DEPLOY_USER}@${PROD_SERVER} '
|
||||
PREV=\$(ls -1dt /home/webservice/api/releases/*/ | sed -n "2p" | xargs basename 2>/dev/null) &&
|
||||
[ -n "\$PREV" ] && ln -sfn /home/webservice/api/releases/\$PREV /home/webservice/api/current &&
|
||||
sudo systemctl reload php8.4-fpm
|
||||
' || true
|
||||
"""
|
||||
}
|
||||
}
|
||||
} else {
|
||||
slackSend channel: '#deploy_api', color: 'danger', tokenCredentialId: 'slack-token',
|
||||
message: "❌ *api* ${params.ROLLBACK_TARGET} 롤백 실패\n<${env.BUILD_URL}|빌드 #${env.BUILD_NUMBER}>"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 논리적 데이터베이스 관계 문서
|
||||
|
||||
> **자동 생성**: 2026-03-07 02:57:21
|
||||
> **자동 생성**: 2026-03-12 13:58:25
|
||||
> **소스**: Eloquent 모델 관계 분석
|
||||
|
||||
## 📊 모델별 관계 현황
|
||||
@@ -26,6 +26,68 @@ ### bad_debt_memos
|
||||
- **badDebt()**: belongsTo → `bad_debts`
|
||||
- **creator()**: belongsTo → `users`
|
||||
|
||||
### barobill_bank_sync_status
|
||||
**모델**: `App\Models\Barobill\BarobillBankSyncStatus`
|
||||
|
||||
- **tenant()**: belongsTo → `tenants`
|
||||
|
||||
### barobill_bank_transactions
|
||||
**모델**: `App\Models\Barobill\BarobillBankTransaction`
|
||||
|
||||
- **tenant()**: belongsTo → `tenants`
|
||||
|
||||
### barobill_bank_transaction_splits
|
||||
**모델**: `App\Models\Barobill\BarobillBankTransactionSplit`
|
||||
|
||||
- **tenant()**: belongsTo → `tenants`
|
||||
|
||||
### barobill_billing_records
|
||||
**모델**: `App\Models\Barobill\BarobillBillingRecord`
|
||||
|
||||
- **member()**: belongsTo → `barobill_members`
|
||||
|
||||
### barobill_card_transactions
|
||||
**모델**: `App\Models\Barobill\BarobillCardTransaction`
|
||||
|
||||
- **tenant()**: belongsTo → `tenants`
|
||||
|
||||
### barobill_card_transaction_amount_logs
|
||||
**모델**: `App\Models\Barobill\BarobillCardTransactionAmountLog`
|
||||
|
||||
- **cardTransaction()**: belongsTo → `barobill_card_transactions`
|
||||
|
||||
### barobill_card_transaction_splits
|
||||
**모델**: `App\Models\Barobill\BarobillCardTransactionSplit`
|
||||
|
||||
- **tenant()**: belongsTo → `tenants`
|
||||
|
||||
### barobill_members
|
||||
**모델**: `App\Models\Barobill\BarobillMember`
|
||||
|
||||
- **tenant()**: belongsTo → `tenants`
|
||||
|
||||
### barobill_monthly_summarys
|
||||
**모델**: `App\Models\Barobill\BarobillMonthlySummary`
|
||||
|
||||
- **member()**: belongsTo → `barobill_members`
|
||||
|
||||
### barobill_subscriptions
|
||||
**모델**: `App\Models\Barobill\BarobillSubscription`
|
||||
|
||||
- **member()**: belongsTo → `barobill_members`
|
||||
|
||||
### hometax_invoices
|
||||
**모델**: `App\Models\Barobill\HometaxInvoice`
|
||||
|
||||
- **tenant()**: belongsTo → `tenants`
|
||||
- **journals()**: hasMany → `hometax_invoice_journals`
|
||||
|
||||
### hometax_invoice_journals
|
||||
**모델**: `App\Models\Barobill\HometaxInvoiceJournal`
|
||||
|
||||
- **tenant()**: belongsTo → `tenants`
|
||||
- **invoice()**: belongsTo → `hometax_invoices`
|
||||
|
||||
### biddings
|
||||
**모델**: `App\Models\Bidding\Bidding`
|
||||
|
||||
@@ -309,6 +371,43 @@ ### esign_signers
|
||||
- **contract()**: belongsTo → `esign_contracts`
|
||||
- **signFields()**: hasMany → `esign_sign_fields`
|
||||
|
||||
### equipments
|
||||
**모델**: `App\Models\Equipment\Equipment`
|
||||
|
||||
- **inspectionTemplates()**: hasMany → `equipment_inspection_templates`
|
||||
- **inspections()**: hasMany → `equipment_inspections`
|
||||
- **repairs()**: hasMany → `equipment_repairs`
|
||||
- **photos()**: hasMany → `files`
|
||||
- **processes()**: belongsToMany → `processes`
|
||||
|
||||
### equipment_inspections
|
||||
**모델**: `App\Models\Equipment\EquipmentInspection`
|
||||
|
||||
- **equipment()**: belongsTo → `equipments`
|
||||
- **details()**: hasMany → `equipment_inspection_details`
|
||||
|
||||
### equipment_inspection_details
|
||||
**모델**: `App\Models\Equipment\EquipmentInspectionDetail`
|
||||
|
||||
- **inspection()**: belongsTo → `equipment_inspections`
|
||||
- **templateItem()**: belongsTo → `equipment_inspection_templates`
|
||||
|
||||
### equipment_inspection_templates
|
||||
**모델**: `App\Models\Equipment\EquipmentInspectionTemplate`
|
||||
|
||||
- **equipment()**: belongsTo → `equipments`
|
||||
|
||||
### equipment_process
|
||||
**모델**: `App\Models\Equipment\EquipmentProcess`
|
||||
|
||||
- **equipment()**: belongsTo → `equipments`
|
||||
- **process()**: belongsTo → `processes`
|
||||
|
||||
### equipment_repairs
|
||||
**모델**: `App\Models\Equipment\EquipmentRepair`
|
||||
|
||||
- **equipment()**: belongsTo → `equipments`
|
||||
|
||||
### estimates
|
||||
**모델**: `App\Models\Estimate\Estimate`
|
||||
|
||||
@@ -734,6 +833,36 @@ ### push_notification_settings
|
||||
**모델**: `App\Models\PushNotificationSetting`
|
||||
|
||||
|
||||
### audit_checklists
|
||||
**모델**: `App\Models\Qualitys\AuditChecklist`
|
||||
|
||||
- **categories()**: hasMany → `audit_checklist_categories`
|
||||
|
||||
### audit_checklist_categorys
|
||||
**모델**: `App\Models\Qualitys\AuditChecklistCategory`
|
||||
|
||||
- **checklist()**: belongsTo → `audit_checklists`
|
||||
- **items()**: hasMany → `audit_checklist_items`
|
||||
|
||||
### audit_checklist_items
|
||||
**모델**: `App\Models\Qualitys\AuditChecklistItem`
|
||||
|
||||
- **category()**: belongsTo → `audit_checklist_categories`
|
||||
- **standardDocuments()**: hasMany → `audit_standard_documents`
|
||||
|
||||
### audit_standard_documents
|
||||
**모델**: `App\Models\Qualitys\AuditStandardDocument`
|
||||
|
||||
- **checklistItem()**: belongsTo → `audit_checklist_items`
|
||||
- **document()**: belongsTo → `documents`
|
||||
|
||||
### checklist_templates
|
||||
**모델**: `App\Models\Qualitys\ChecklistTemplate`
|
||||
|
||||
- **creator()**: belongsTo → `users`
|
||||
- **updater()**: belongsTo → `users`
|
||||
- **documents()**: morphMany → `files`
|
||||
|
||||
### inspections
|
||||
**모델**: `App\Models\Qualitys\Inspection`
|
||||
|
||||
@@ -838,6 +967,11 @@ ### quote_revisions
|
||||
- **quote()**: belongsTo → `quotes`
|
||||
- **reviser()**: belongsTo → `users`
|
||||
|
||||
### account_codes
|
||||
**모델**: `App\Models\Tenants\AccountCode`
|
||||
|
||||
- **children()**: hasMany → `account_codes`
|
||||
|
||||
### ai_reports
|
||||
**모델**: `App\Models\Tenants\AiReport`
|
||||
|
||||
@@ -857,14 +991,24 @@ ### approvals
|
||||
**모델**: `App\Models\Tenants\Approval`
|
||||
|
||||
- **form()**: belongsTo → `approval_forms`
|
||||
- **line()**: belongsTo → `approval_lines`
|
||||
- **drafter()**: belongsTo → `users`
|
||||
- **department()**: belongsTo → `departments`
|
||||
- **parentDocument()**: belongsTo → `approvals`
|
||||
- **creator()**: belongsTo → `users`
|
||||
- **updater()**: belongsTo → `users`
|
||||
- **childDocuments()**: hasMany → `approvals`
|
||||
- **steps()**: hasMany → `approval_steps`
|
||||
- **approverSteps()**: hasMany → `approval_steps`
|
||||
- **referenceSteps()**: hasMany → `approval_steps`
|
||||
- **linkable()**: morphTo → `(Polymorphic)`
|
||||
|
||||
### approval_delegations
|
||||
**모델**: `App\Models\Tenants\ApprovalDelegation`
|
||||
|
||||
- **delegator()**: belongsTo → `users`
|
||||
- **delegate()**: belongsTo → `users`
|
||||
|
||||
### approval_forms
|
||||
**모델**: `App\Models\Tenants\ApprovalForm`
|
||||
|
||||
@@ -883,6 +1027,7 @@ ### approval_steps
|
||||
|
||||
- **approval()**: belongsTo → `approvals`
|
||||
- **approver()**: belongsTo → `users`
|
||||
- **actedBy()**: belongsTo → `users`
|
||||
|
||||
### attendances
|
||||
**모델**: `App\Models\Tenants\Attendance`
|
||||
@@ -1004,6 +1149,11 @@ ### loans
|
||||
- **creator()**: belongsTo → `users`
|
||||
- **updater()**: belongsTo → `users`
|
||||
|
||||
### mail_logs
|
||||
**모델**: `App\Models\Tenants\MailLog`
|
||||
|
||||
- **tenant()**: belongsTo → `tenants`
|
||||
|
||||
### payments
|
||||
**모델**: `App\Models\Tenants\Payment`
|
||||
|
||||
@@ -1046,6 +1196,7 @@ ### receivings
|
||||
**모델**: `App\Models\Tenants\Receiving`
|
||||
|
||||
- **item()**: belongsTo → `items`
|
||||
- **certificateFile()**: belongsTo → `files`
|
||||
- **creator()**: belongsTo → `users`
|
||||
|
||||
### salarys
|
||||
@@ -1167,6 +1318,11 @@ ### tenant_field_settings
|
||||
- **fieldDef()**: belongsTo → `setting_field_defs`
|
||||
- **optionGroup()**: belongsTo → `tenant_option_groups`
|
||||
|
||||
### tenant_mail_configs
|
||||
**모델**: `App\Models\Tenants\TenantMailConfig`
|
||||
|
||||
- **tenant()**: belongsTo → `tenants`
|
||||
|
||||
### tenant_option_groups
|
||||
**모델**: `App\Models\Tenants\TenantOptionGroup`
|
||||
|
||||
|
||||
@@ -40,8 +40,8 @@ public function handle(): int
|
||||
foreach ($files as $file) {
|
||||
try {
|
||||
// Delete physical file
|
||||
if (Storage::disk('tenant')->exists($file->file_path)) {
|
||||
Storage::disk('tenant')->delete($file->file_path);
|
||||
if (Storage::disk('r2')->exists($file->file_path)) {
|
||||
Storage::disk('r2')->delete($file->file_path);
|
||||
}
|
||||
|
||||
// Force delete from DB
|
||||
|
||||
@@ -60,8 +60,8 @@ private function permanentDelete(File $file): void
|
||||
{
|
||||
DB::transaction(function () use ($file) {
|
||||
// Delete physical file
|
||||
if (Storage::disk('tenant')->exists($file->file_path)) {
|
||||
Storage::disk('tenant')->delete($file->file_path);
|
||||
if (Storage::disk('r2')->exists($file->file_path)) {
|
||||
Storage::disk('r2')->delete($file->file_path);
|
||||
}
|
||||
|
||||
// Update tenant storage usage
|
||||
|
||||
264
app/Console/Commands/UploadLocalFilesToR2.php
Normal file
264
app/Console/Commands/UploadLocalFilesToR2.php
Normal file
@@ -0,0 +1,264 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\Commons\File;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Facades\File as FileFacade;
|
||||
|
||||
class UploadLocalFilesToR2 extends Command
|
||||
{
|
||||
protected $signature = 'r2:upload-local
|
||||
{--count=3 : Number of files to upload}
|
||||
{--source=db : Source: "db" (latest DB records) or "disk" (latest local files)}
|
||||
{--dry-run : Show files without uploading}
|
||||
{--fix : Delete wrong-path files from R2 before re-uploading}';
|
||||
|
||||
protected $description = 'Upload local files to Cloudflare R2 (by DB records or local disk)';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$count = (int) $this->option('count');
|
||||
$source = $this->option('source');
|
||||
$dryRun = $this->option('dry-run');
|
||||
|
||||
$this->info("=== R2 Upload Tool ===");
|
||||
$this->info("Source: {$source} | Count: {$count}");
|
||||
|
||||
return $source === 'db'
|
||||
? $this->uploadFromDb($count, $dryRun)
|
||||
: $this->uploadFromDisk($count, $dryRun);
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload files based on DB records (latest by ID desc)
|
||||
*/
|
||||
private function uploadFromDb(int $count, bool $dryRun): int
|
||||
{
|
||||
$files = File::orderByDesc('id')->limit($count)->get();
|
||||
|
||||
if ($files->isEmpty()) {
|
||||
$this->warn('No files in DB.');
|
||||
return 0;
|
||||
}
|
||||
|
||||
$this->newLine();
|
||||
$headers = ['ID', 'Display Name', 'R2 Path', 'R2 Exists', 'Local Exists', 'Size'];
|
||||
$rows = [];
|
||||
|
||||
foreach ($files as $f) {
|
||||
$localPath = storage_path("app/tenants/{$f->file_path}");
|
||||
$r2Exists = Storage::disk('r2')->exists($f->file_path);
|
||||
$localExists = file_exists($localPath);
|
||||
|
||||
$rows[] = [
|
||||
$f->id,
|
||||
mb_strimwidth($f->display_name ?? '', 0, 25, '...'),
|
||||
$f->file_path,
|
||||
$r2Exists ? '✓ YES' : '✗ NO',
|
||||
$localExists ? '✓ YES' : '✗ NO',
|
||||
$f->file_size ? $this->formatSize($f->file_size) : '-',
|
||||
];
|
||||
}
|
||||
|
||||
$this->table($headers, $rows);
|
||||
|
||||
// Filter: local exists but R2 doesn't
|
||||
$toUpload = $files->filter(function ($f) {
|
||||
$localPath = storage_path("app/tenants/{$f->file_path}");
|
||||
return file_exists($localPath) && !Storage::disk('r2')->exists($f->file_path);
|
||||
});
|
||||
|
||||
$alreadyInR2 = $files->filter(function ($f) {
|
||||
return Storage::disk('r2')->exists($f->file_path);
|
||||
});
|
||||
|
||||
if ($alreadyInR2->isNotEmpty()) {
|
||||
$this->info("Already in R2: {$alreadyInR2->count()} files (skipped)");
|
||||
}
|
||||
|
||||
if ($toUpload->isEmpty()) {
|
||||
$missingBoth = $files->filter(function ($f) {
|
||||
$localPath = storage_path("app/tenants/{$f->file_path}");
|
||||
return !file_exists($localPath) && !Storage::disk('r2')->exists($f->file_path);
|
||||
});
|
||||
|
||||
if ($missingBoth->isNotEmpty()) {
|
||||
$this->warn("Missing both locally and in R2: {$missingBoth->count()} files");
|
||||
$this->warn("These files may exist on the dev server only.");
|
||||
}
|
||||
|
||||
$this->info('Nothing to upload.');
|
||||
return 0;
|
||||
}
|
||||
|
||||
if ($dryRun) {
|
||||
$this->warn("[DRY RUN] Would upload {$toUpload->count()} files.");
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Test R2 connection
|
||||
$this->info('Testing R2 connection...');
|
||||
try {
|
||||
Storage::disk('r2')->directories('/');
|
||||
$this->info('✓ R2 connection OK');
|
||||
} catch (\Exception $e) {
|
||||
$this->error('✗ R2 connection failed: ' . $e->getMessage());
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Upload
|
||||
$bar = $this->output->createProgressBar($toUpload->count());
|
||||
$bar->start();
|
||||
$success = 0;
|
||||
$failed = 0;
|
||||
|
||||
foreach ($toUpload as $f) {
|
||||
$localPath = storage_path("app/tenants/{$f->file_path}");
|
||||
try {
|
||||
$content = FileFacade::get($localPath);
|
||||
$mimeType = $f->mime_type ?: FileFacade::mimeType($localPath);
|
||||
|
||||
Storage::disk('r2')->put($f->file_path, $content, [
|
||||
'ContentType' => $mimeType,
|
||||
]);
|
||||
$success++;
|
||||
} catch (\Exception $e) {
|
||||
$failed++;
|
||||
$this->newLine();
|
||||
$this->error(" ✗ ID {$f->id}: {$e->getMessage()}");
|
||||
}
|
||||
$bar->advance();
|
||||
}
|
||||
|
||||
$bar->finish();
|
||||
$this->newLine(2);
|
||||
$this->info("=== Upload Complete ===");
|
||||
$this->info("✓ Success: {$success}");
|
||||
if ($failed > 0) {
|
||||
$this->error("✗ Failed: {$failed}");
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload files based on local disk (newest files by mtime)
|
||||
*/
|
||||
private function uploadFromDisk(int $count, bool $dryRun): int
|
||||
{
|
||||
$storagePath = storage_path('app/tenants');
|
||||
|
||||
if (!is_dir($storagePath)) {
|
||||
$this->error("Path not found: {$storagePath}");
|
||||
return 1;
|
||||
}
|
||||
|
||||
$allFiles = $this->collectFiles($storagePath);
|
||||
if (empty($allFiles)) {
|
||||
$this->warn('No files found.');
|
||||
return 0;
|
||||
}
|
||||
|
||||
usort($allFiles, fn($a, $b) => filemtime($b) - filemtime($a));
|
||||
$filesToUpload = array_slice($allFiles, 0, $count);
|
||||
|
||||
$this->info("Found " . count($allFiles) . " total files, uploading {$count} most recent:");
|
||||
$this->newLine();
|
||||
|
||||
$headers = ['#', 'File', 'Size', 'Modified', 'R2 Path'];
|
||||
$rows = [];
|
||||
|
||||
foreach ($filesToUpload as $i => $filePath) {
|
||||
$r2Path = $this->toR2Path($filePath);
|
||||
$rows[] = [$i + 1, basename($filePath), $this->formatSize(filesize($filePath)), date('Y-m-d H:i:s', filemtime($filePath)), $r2Path];
|
||||
}
|
||||
|
||||
$this->table($headers, $rows);
|
||||
|
||||
if ($dryRun) {
|
||||
$this->warn('[DRY RUN] No files uploaded.');
|
||||
return 0;
|
||||
}
|
||||
|
||||
$this->info('Testing R2 connection...');
|
||||
try {
|
||||
Storage::disk('r2')->directories('/');
|
||||
$this->info('✓ R2 connection OK');
|
||||
} catch (\Exception $e) {
|
||||
$this->error('✗ R2 connection failed: ' . $e->getMessage());
|
||||
return 1;
|
||||
}
|
||||
|
||||
$bar = $this->output->createProgressBar(count($filesToUpload));
|
||||
$bar->start();
|
||||
$success = 0;
|
||||
$failed = 0;
|
||||
$fix = $this->option('fix');
|
||||
|
||||
foreach ($filesToUpload as $filePath) {
|
||||
$r2Path = $this->toR2Path($filePath);
|
||||
try {
|
||||
if ($fix) {
|
||||
$wrongPath = $this->toRelativePath($filePath);
|
||||
if ($wrongPath !== $r2Path && Storage::disk('r2')->exists($wrongPath)) {
|
||||
Storage::disk('r2')->delete($wrongPath);
|
||||
}
|
||||
}
|
||||
|
||||
$content = FileFacade::get($filePath);
|
||||
$mimeType = FileFacade::mimeType($filePath);
|
||||
|
||||
Storage::disk('r2')->put($r2Path, $content, ['ContentType' => $mimeType]);
|
||||
$success++;
|
||||
} catch (\Exception $e) {
|
||||
$failed++;
|
||||
$this->newLine();
|
||||
$this->error(" ✗ Failed: {$r2Path} - {$e->getMessage()}");
|
||||
}
|
||||
$bar->advance();
|
||||
}
|
||||
|
||||
$bar->finish();
|
||||
$this->newLine(2);
|
||||
$this->info("=== Upload Complete ===");
|
||||
$this->info("✓ Success: {$success}");
|
||||
if ($failed > 0) {
|
||||
$this->error("✗ Failed: {$failed}");
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
private function toR2Path(string $filePath): string
|
||||
{
|
||||
$relative = $this->toRelativePath($filePath);
|
||||
return str_starts_with($relative, 'tenants/') ? substr($relative, strlen('tenants/')) : $relative;
|
||||
}
|
||||
|
||||
private function toRelativePath(string $filePath): string
|
||||
{
|
||||
return str_replace(str_replace('\\', '/', storage_path('app/')), '', str_replace('\\', '/', $filePath));
|
||||
}
|
||||
|
||||
private function collectFiles(string $dir): array
|
||||
{
|
||||
$files = [];
|
||||
$iterator = new \RecursiveIteratorIterator(
|
||||
new \RecursiveDirectoryIterator($dir, \RecursiveDirectoryIterator::SKIP_DOTS)
|
||||
);
|
||||
foreach ($iterator as $file) {
|
||||
if ($file->isFile() && $file->getFilename() !== '.gitignore') {
|
||||
$files[] = $file->getPathname();
|
||||
}
|
||||
}
|
||||
return $files;
|
||||
}
|
||||
|
||||
private function formatSize(int $bytes): string
|
||||
{
|
||||
if ($bytes >= 1048576) return round($bytes / 1048576, 1) . ' MB';
|
||||
return round($bytes / 1024, 1) . ' KB';
|
||||
}
|
||||
}
|
||||
232
app/Enums/InspectionCycle.php
Normal file
232
app/Enums/InspectionCycle.php
Normal file
@@ -0,0 +1,232 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
use App\Models\Commons\Holiday;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class InspectionCycle
|
||||
{
|
||||
const DAILY = 'daily';
|
||||
|
||||
const WEEKLY = 'weekly';
|
||||
|
||||
const MONTHLY = 'monthly';
|
||||
|
||||
const BIMONTHLY = 'bimonthly';
|
||||
|
||||
const QUARTERLY = 'quarterly';
|
||||
|
||||
const SEMIANNUAL = 'semiannual';
|
||||
|
||||
public static function all(): array
|
||||
{
|
||||
return [
|
||||
self::DAILY => '일일',
|
||||
self::WEEKLY => '주간',
|
||||
self::MONTHLY => '월간',
|
||||
self::BIMONTHLY => '2개월',
|
||||
self::QUARTERLY => '분기',
|
||||
self::SEMIANNUAL => '반년',
|
||||
];
|
||||
}
|
||||
|
||||
public static function label(string $cycle): string
|
||||
{
|
||||
return self::all()[$cycle] ?? $cycle;
|
||||
}
|
||||
|
||||
public static function periodType(string $cycle): string
|
||||
{
|
||||
return $cycle === self::DAILY ? 'month' : 'year';
|
||||
}
|
||||
|
||||
public static function columnLabels(string $cycle, ?string $period = null): array
|
||||
{
|
||||
return match ($cycle) {
|
||||
self::DAILY => self::dailyLabels($period),
|
||||
self::WEEKLY => self::weeklyLabels(),
|
||||
self::MONTHLY => self::monthlyLabels(),
|
||||
self::BIMONTHLY => self::bimonthlyLabels(),
|
||||
self::QUARTERLY => self::quarterlyLabels(),
|
||||
self::SEMIANNUAL => self::semiannualLabels(),
|
||||
default => self::dailyLabels($period),
|
||||
};
|
||||
}
|
||||
|
||||
public static function resolveCheckDate(string $cycle, string $period, int $colIndex): string
|
||||
{
|
||||
return match ($cycle) {
|
||||
self::DAILY => self::dailyCheckDate($period, $colIndex),
|
||||
self::WEEKLY => self::weeklyCheckDate($period, $colIndex),
|
||||
self::MONTHLY => self::monthlyCheckDate($period, $colIndex),
|
||||
self::BIMONTHLY => self::bimonthlyCheckDate($period, $colIndex),
|
||||
self::QUARTERLY => self::quarterlyCheckDate($period, $colIndex),
|
||||
self::SEMIANNUAL => self::semiannualCheckDate($period, $colIndex),
|
||||
default => self::dailyCheckDate($period, $colIndex),
|
||||
};
|
||||
}
|
||||
|
||||
public static function resolvePeriod(string $cycle, string $checkDate): string
|
||||
{
|
||||
$date = Carbon::parse($checkDate);
|
||||
|
||||
return match ($cycle) {
|
||||
self::DAILY => $date->format('Y-m'),
|
||||
self::WEEKLY => (string) $date->isoWeekYear,
|
||||
default => $date->format('Y'),
|
||||
};
|
||||
}
|
||||
|
||||
public static function columnCount(string $cycle, ?string $period = null): int
|
||||
{
|
||||
return count(self::columnLabels($cycle, $period));
|
||||
}
|
||||
|
||||
public static function isWeekend(string $period, int $colIndex): bool
|
||||
{
|
||||
$date = Carbon::createFromFormat('Y-m', $period)->startOfMonth()->addDays($colIndex - 1);
|
||||
|
||||
return in_array($date->dayOfWeek, [0, 6]);
|
||||
}
|
||||
|
||||
public static function getHolidayDates(string $cycle, string $period, int $tenantId): array
|
||||
{
|
||||
if ($cycle === self::DAILY) {
|
||||
$start = Carbon::createFromFormat('Y-m', $period)->startOfMonth();
|
||||
$end = $start->copy()->endOfMonth();
|
||||
} else {
|
||||
$start = Carbon::create((int) $period, 1, 1);
|
||||
$end = Carbon::create((int) $period, 12, 31);
|
||||
}
|
||||
|
||||
$holidays = Holiday::where('tenant_id', $tenantId)
|
||||
->where('start_date', '<=', $end->toDateString())
|
||||
->where('end_date', '>=', $start->toDateString())
|
||||
->get();
|
||||
|
||||
$dates = [];
|
||||
foreach ($holidays as $holiday) {
|
||||
$hStart = $holiday->start_date->copy()->max($start);
|
||||
$hEnd = $holiday->end_date->copy()->min($end);
|
||||
$current = $hStart->copy();
|
||||
while ($current->lte($hEnd)) {
|
||||
$dates[$current->format('Y-m-d')] = true;
|
||||
$current->addDay();
|
||||
}
|
||||
}
|
||||
|
||||
return $dates;
|
||||
}
|
||||
|
||||
public static function isNonWorkingDay(string $checkDate, array $holidayDates = []): bool
|
||||
{
|
||||
$date = Carbon::parse($checkDate);
|
||||
|
||||
return $date->isWeekend() || isset($holidayDates[$checkDate]);
|
||||
}
|
||||
|
||||
// --- Daily ---
|
||||
private static function dailyLabels(?string $period): array
|
||||
{
|
||||
$date = Carbon::createFromFormat('Y-m', $period ?? now()->format('Y-m'));
|
||||
$days = $date->daysInMonth;
|
||||
$labels = [];
|
||||
for ($d = 1; $d <= $days; $d++) {
|
||||
$labels[$d] = (string) $d;
|
||||
}
|
||||
|
||||
return $labels;
|
||||
}
|
||||
|
||||
private static function dailyCheckDate(string $period, int $colIndex): string
|
||||
{
|
||||
return Carbon::createFromFormat('Y-m', $period)->startOfMonth()->addDays($colIndex - 1)->format('Y-m-d');
|
||||
}
|
||||
|
||||
// --- Weekly ---
|
||||
private static function weeklyLabels(): array
|
||||
{
|
||||
$labels = [];
|
||||
for ($w = 1; $w <= 52; $w++) {
|
||||
$labels[$w] = $w.'주';
|
||||
}
|
||||
|
||||
return $labels;
|
||||
}
|
||||
|
||||
private static function weeklyCheckDate(string $year, int $colIndex): string
|
||||
{
|
||||
return Carbon::create((int) $year)->setISODate((int) $year, $colIndex, 1)->format('Y-m-d');
|
||||
}
|
||||
|
||||
// --- Monthly ---
|
||||
private static function monthlyLabels(): array
|
||||
{
|
||||
$labels = [];
|
||||
for ($m = 1; $m <= 12; $m++) {
|
||||
$labels[$m] = $m.'월';
|
||||
}
|
||||
|
||||
return $labels;
|
||||
}
|
||||
|
||||
private static function monthlyCheckDate(string $year, int $colIndex): string
|
||||
{
|
||||
return Carbon::create((int) $year, $colIndex, 1)->format('Y-m-d');
|
||||
}
|
||||
|
||||
// --- Bimonthly ---
|
||||
private static function bimonthlyLabels(): array
|
||||
{
|
||||
return [
|
||||
1 => '1~2월',
|
||||
2 => '3~4월',
|
||||
3 => '5~6월',
|
||||
4 => '7~8월',
|
||||
5 => '9~10월',
|
||||
6 => '11~12월',
|
||||
];
|
||||
}
|
||||
|
||||
private static function bimonthlyCheckDate(string $year, int $colIndex): string
|
||||
{
|
||||
$month = ($colIndex - 1) * 2 + 1;
|
||||
|
||||
return Carbon::create((int) $year, $month, 1)->format('Y-m-d');
|
||||
}
|
||||
|
||||
// --- Quarterly ---
|
||||
private static function quarterlyLabels(): array
|
||||
{
|
||||
return [
|
||||
1 => '1분기',
|
||||
2 => '2분기',
|
||||
3 => '3분기',
|
||||
4 => '4분기',
|
||||
];
|
||||
}
|
||||
|
||||
private static function quarterlyCheckDate(string $year, int $colIndex): string
|
||||
{
|
||||
$month = ($colIndex - 1) * 3 + 1;
|
||||
|
||||
return Carbon::create((int) $year, $month, 1)->format('Y-m-d');
|
||||
}
|
||||
|
||||
// --- Semiannual ---
|
||||
private static function semiannualLabels(): array
|
||||
{
|
||||
return [
|
||||
1 => '상반기',
|
||||
2 => '하반기',
|
||||
];
|
||||
}
|
||||
|
||||
private static function semiannualCheckDate(string $year, int $colIndex): string
|
||||
{
|
||||
$month = $colIndex === 1 ? 1 : 7;
|
||||
|
||||
return Carbon::create((int) $year, $month, 1)->format('Y-m-d');
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
use App\Helpers\ApiResponse;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\V1\AccountSubject\StoreAccountSubjectRequest;
|
||||
use App\Http\Requests\V1\AccountSubject\UpdateAccountSubjectRequest;
|
||||
use App\Services\AccountCodeService;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
@@ -19,7 +20,10 @@ public function __construct(
|
||||
*/
|
||||
public function index(Request $request)
|
||||
{
|
||||
$params = $request->only(['search', 'category']);
|
||||
$params = $request->only([
|
||||
'search', 'category', 'sub_category',
|
||||
'department_type', 'depth', 'is_active', 'selectable',
|
||||
]);
|
||||
|
||||
$subjects = $this->service->index($params);
|
||||
|
||||
@@ -36,6 +40,16 @@ public function store(StoreAccountSubjectRequest $request)
|
||||
return ApiResponse::success($subject, __('message.created'), [], 201);
|
||||
}
|
||||
|
||||
/**
|
||||
* 계정과목 수정
|
||||
*/
|
||||
public function update(int $id, UpdateAccountSubjectRequest $request)
|
||||
{
|
||||
$subject = $this->service->update($id, $request->validated());
|
||||
|
||||
return ApiResponse::success($subject, __('message.updated'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 계정과목 활성/비활성 토글
|
||||
*/
|
||||
@@ -57,4 +71,17 @@ public function destroy(int $id)
|
||||
|
||||
return ApiResponse::success(null, __('message.deleted'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 기본 계정과목표 일괄 생성 (더존 표준)
|
||||
*/
|
||||
public function seedDefaults()
|
||||
{
|
||||
$count = $this->service->seedDefaults();
|
||||
|
||||
return ApiResponse::success(
|
||||
['inserted_count' => $count],
|
||||
__('message.created')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,14 @@
|
||||
|
||||
use App\Helpers\ApiResponse;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Approval\ApproveRequest;
|
||||
use App\Http\Requests\Approval\CancelRequest;
|
||||
use App\Http\Requests\Approval\DelegationStoreRequest;
|
||||
use App\Http\Requests\Approval\DelegationUpdateRequest;
|
||||
use App\Http\Requests\Approval\HoldRequest;
|
||||
use App\Http\Requests\Approval\InboxIndexRequest;
|
||||
use App\Http\Requests\Approval\IndexRequest;
|
||||
use App\Http\Requests\Approval\PreDecideRequest;
|
||||
use App\Http\Requests\Approval\ReferenceIndexRequest;
|
||||
use App\Http\Requests\Approval\RejectRequest;
|
||||
use App\Http\Requests\Approval\StoreRequest;
|
||||
@@ -133,10 +139,10 @@ public function submit(int $id, SubmitRequest $request): JsonResponse
|
||||
* 결재 승인
|
||||
* POST /v1/approvals/{id}/approve
|
||||
*/
|
||||
public function approve(int $id, Request $request): JsonResponse
|
||||
public function approve(int $id, ApproveRequest $request): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id, $request) {
|
||||
return $this->service->approve($id, $request->input('comment'));
|
||||
return $this->service->approve($id, $request->validated()['comment'] ?? null);
|
||||
}, __('message.approval.approved'));
|
||||
}
|
||||
|
||||
@@ -155,11 +161,99 @@ public function reject(int $id, RejectRequest $request): JsonResponse
|
||||
* 결재 회수 (기안자만)
|
||||
* POST /v1/approvals/{id}/cancel
|
||||
*/
|
||||
public function cancel(int $id): JsonResponse
|
||||
public function cancel(int $id, CancelRequest $request): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id, $request) {
|
||||
return $this->service->cancel($id, $request->validated()['recall_reason'] ?? null);
|
||||
}, __('message.approval.cancelled'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 보류 (현재 결재자만)
|
||||
* POST /v1/approvals/{id}/hold
|
||||
*/
|
||||
public function hold(int $id, HoldRequest $request): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id, $request) {
|
||||
return $this->service->hold($id, $request->validated()['comment']);
|
||||
}, __('message.approval.held'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 보류 해제 (보류한 결재자만)
|
||||
* POST /v1/approvals/{id}/release-hold
|
||||
*/
|
||||
public function releaseHold(int $id): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id) {
|
||||
return $this->service->cancel($id);
|
||||
}, __('message.approval.cancelled'));
|
||||
return $this->service->releaseHold($id);
|
||||
}, __('message.approval.hold_released'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 전결 (현재 결재자가 이후 모든 결재를 건너뛰고 최종 승인)
|
||||
* POST /v1/approvals/{id}/pre-decide
|
||||
*/
|
||||
public function preDecide(int $id, PreDecideRequest $request): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id, $request) {
|
||||
return $this->service->preDecide($id, $request->validated()['comment'] ?? null);
|
||||
}, __('message.approval.pre_decided'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 복사 재기안
|
||||
* POST /v1/approvals/{id}/copy
|
||||
*/
|
||||
public function copyForRedraft(int $id): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id) {
|
||||
return $this->service->copyForRedraft($id);
|
||||
}, __('message.approval.copied'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 완료함 목록
|
||||
* GET /v1/approvals/completed
|
||||
*/
|
||||
public function completed(IndexRequest $request): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
return $this->service->completed($request->validated());
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 완료함 현황 카드
|
||||
* GET /v1/approvals/completed/summary
|
||||
*/
|
||||
public function completedSummary(): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () {
|
||||
return $this->service->completedSummary();
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 미처리 건수 (뱃지용)
|
||||
* GET /v1/approvals/badge-counts
|
||||
*/
|
||||
public function badgeCounts(): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () {
|
||||
return $this->service->badgeCounts();
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 완료함 미읽음 일괄 읽음 처리
|
||||
* POST /v1/approvals/completed/mark-read
|
||||
*/
|
||||
public function markCompletedAsRead(): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () {
|
||||
return $this->service->markCompletedAsRead();
|
||||
}, __('message.approval.marked_read'));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -183,4 +277,52 @@ public function markUnread(int $id): JsonResponse
|
||||
return $this->service->markUnread($id);
|
||||
}, __('message.approval.marked_unread'));
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 위임 관리
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 위임 목록
|
||||
* GET /v1/approvals/delegations
|
||||
*/
|
||||
public function delegationIndex(Request $request): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
return $this->service->delegationIndex($request->all());
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 위임 생성
|
||||
* POST /v1/approvals/delegations
|
||||
*/
|
||||
public function delegationStore(DelegationStoreRequest $request): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
return $this->service->delegationStore($request->validated());
|
||||
}, __('message.created'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 위임 수정
|
||||
* PATCH /v1/approvals/delegations/{id}
|
||||
*/
|
||||
public function delegationUpdate(int $id, DelegationUpdateRequest $request): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id, $request) {
|
||||
return $this->service->delegationUpdate($id, $request->validated());
|
||||
}, __('message.updated'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 위임 삭제
|
||||
* DELETE /v1/approvals/delegations/{id}
|
||||
*/
|
||||
public function delegationDestroy(int $id): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id) {
|
||||
return $this->service->delegationDestroy($id);
|
||||
}, __('message.deleted'));
|
||||
}
|
||||
}
|
||||
|
||||
112
app/Http/Controllers/Api/V1/AuditChecklistController.php
Normal file
112
app/Http/Controllers/Api/V1/AuditChecklistController.php
Normal file
@@ -0,0 +1,112 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use App\Helpers\ApiResponse;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Qms\AuditChecklistStoreRequest;
|
||||
use App\Http\Requests\Qms\AuditChecklistUpdateRequest;
|
||||
use App\Services\AuditChecklistService;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class AuditChecklistController extends Controller
|
||||
{
|
||||
public function __construct(private AuditChecklistService $service) {}
|
||||
|
||||
/**
|
||||
* 점검표 목록
|
||||
*/
|
||||
public function index(Request $request)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
return $this->service->index($request->all());
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 점검표 생성 (카테고리+항목 일괄)
|
||||
*/
|
||||
public function store(AuditChecklistStoreRequest $request)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
return $this->service->store($request->validated());
|
||||
}, __('message.created'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 점검표 상세
|
||||
*/
|
||||
public function show(int $id)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id) {
|
||||
return $this->service->show($id);
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 점검표 수정
|
||||
*/
|
||||
public function update(AuditChecklistUpdateRequest $request, int $id)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request, $id) {
|
||||
return $this->service->update($id, $request->validated());
|
||||
}, __('message.updated'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 점검표 완료 처리
|
||||
*/
|
||||
public function complete(int $id)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id) {
|
||||
return $this->service->complete($id);
|
||||
}, __('message.updated'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 항목 완료/미완료 토글
|
||||
*/
|
||||
public function toggleItem(int $id)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id) {
|
||||
return $this->service->toggleItem($id);
|
||||
}, __('message.updated'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 항목별 기준 문서 조회
|
||||
*/
|
||||
public function itemDocuments(int $id)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id) {
|
||||
return $this->service->itemDocuments($id);
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 기준 문서 연결
|
||||
*/
|
||||
public function attachDocument(Request $request, int $id)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request, $id) {
|
||||
return $this->service->attachDocument($id, $request->validate([
|
||||
'title' => 'required|string|max:200',
|
||||
'version' => 'nullable|string|max:20',
|
||||
'date' => 'nullable|date',
|
||||
'document_id' => 'nullable|integer|exists:documents,id',
|
||||
]));
|
||||
}, __('message.created'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 기준 문서 연결 해제
|
||||
*/
|
||||
public function detachDocument(int $id, int $docId)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id, $docId) {
|
||||
$this->service->detachDocument($id, $docId);
|
||||
|
||||
return null;
|
||||
}, __('message.deleted'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,287 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use App\Helpers\ApiResponse;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Tenants\JournalEntry;
|
||||
use App\Services\BarobillBankTransactionService;
|
||||
use App\Services\JournalSyncService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
/**
|
||||
* 바로빌 은행 거래 API 컨트롤러 (React 연동용)
|
||||
*
|
||||
* MNG에서 동기화된 은행 거래 데이터를 React에서 조회/관리
|
||||
*/
|
||||
class BarobillBankTransactionController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
protected BarobillBankTransactionService $service,
|
||||
protected JournalSyncService $journalSyncService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 은행 거래 목록 조회
|
||||
*/
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
$params = $request->validate([
|
||||
'start_date' => 'nullable|date',
|
||||
'end_date' => 'nullable|date|after_or_equal:start_date',
|
||||
'bank_account_num' => 'nullable|string|max:50',
|
||||
'search' => 'nullable|string|max:100',
|
||||
'per_page' => 'nullable|integer|min:1|max:100',
|
||||
'page' => 'nullable|integer|min:1',
|
||||
]);
|
||||
|
||||
return $this->service->index($params);
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 계좌 목록 (필터용)
|
||||
*/
|
||||
public function accounts(): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () {
|
||||
return $this->service->accounts();
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 잔액 요약
|
||||
*/
|
||||
public function balanceSummary(Request $request): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
$params = $request->validate([
|
||||
'date' => 'nullable|date',
|
||||
]);
|
||||
|
||||
return $this->service->balanceSummary($params);
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 분할 (Splits)
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 거래 분할 조회
|
||||
*/
|
||||
public function getSplits(Request $request): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
$validated = $request->validate([
|
||||
'unique_key' => 'required|string|max:500',
|
||||
]);
|
||||
|
||||
return $this->service->getSplits($validated['unique_key']);
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 거래 분할 저장
|
||||
*/
|
||||
public function saveSplits(Request $request): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
$validated = $request->validate([
|
||||
'unique_key' => 'required|string|max:500',
|
||||
'items' => 'required|array|min:1',
|
||||
'items.*.split_amount' => 'required|numeric',
|
||||
'items.*.account_code' => 'nullable|string|max:20',
|
||||
'items.*.account_name' => 'nullable|string|max:100',
|
||||
'items.*.deduction_type' => 'nullable|string|max:20',
|
||||
'items.*.evidence_name' => 'nullable|string|max:100',
|
||||
'items.*.description' => 'nullable|string|max:500',
|
||||
'items.*.memo' => 'nullable|string|max:500',
|
||||
'items.*.bank_account_num' => 'nullable|string|max:50',
|
||||
'items.*.trans_dt' => 'nullable|string|max:20',
|
||||
'items.*.trans_date' => 'nullable|date',
|
||||
'items.*.original_deposit' => 'nullable|numeric',
|
||||
'items.*.original_withdraw' => 'nullable|numeric',
|
||||
'items.*.summary' => 'nullable|string|max:500',
|
||||
]);
|
||||
|
||||
return $this->service->saveSplits($validated['unique_key'], $validated['items']);
|
||||
}, __('message.created'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 거래 분할 삭제
|
||||
*/
|
||||
public function deleteSplits(Request $request): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
$validated = $request->validate([
|
||||
'unique_key' => 'required|string|max:500',
|
||||
]);
|
||||
|
||||
return $this->service->deleteSplits($validated['unique_key']);
|
||||
}, __('message.deleted'));
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 오버라이드 (Override)
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 적요/분류 오버라이드 저장
|
||||
*/
|
||||
public function saveOverride(Request $request): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
$validated = $request->validate([
|
||||
'unique_key' => 'required|string|max:500',
|
||||
'modified_summary' => 'nullable|string|max:500',
|
||||
'modified_cast' => 'nullable|string|max:100',
|
||||
]);
|
||||
|
||||
return $this->service->saveOverride(
|
||||
$validated['unique_key'],
|
||||
$validated['modified_summary'] ?? null,
|
||||
$validated['modified_cast'] ?? null
|
||||
);
|
||||
}, __('message.updated'));
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 수동 입력 (Manual)
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 수동 은행 거래 등록
|
||||
*/
|
||||
public function storeManual(Request $request): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
$validated = $request->validate([
|
||||
'bank_account_num' => 'required|string|max:50',
|
||||
'bank_code' => 'nullable|string|max:10',
|
||||
'bank_name' => 'nullable|string|max:50',
|
||||
'trans_date' => 'required|date',
|
||||
'trans_time' => 'nullable|string|max:10',
|
||||
'trans_dt' => 'nullable|string|max:20',
|
||||
'deposit' => 'nullable|numeric|min:0',
|
||||
'withdraw' => 'nullable|numeric|min:0',
|
||||
'balance' => 'nullable|numeric',
|
||||
'summary' => 'nullable|string|max:500',
|
||||
'cast' => 'nullable|string|max:100',
|
||||
'memo' => 'nullable|string|max:500',
|
||||
'trans_office' => 'nullable|string|max:100',
|
||||
'account_code' => 'nullable|string|max:20',
|
||||
'account_name' => 'nullable|string|max:100',
|
||||
'client_code' => 'nullable|string|max:20',
|
||||
'client_name' => 'nullable|string|max:200',
|
||||
]);
|
||||
|
||||
return $this->service->storeManual($validated);
|
||||
}, __('message.created'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 수동 은행 거래 수정
|
||||
*/
|
||||
public function updateManual(Request $request, int $id): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request, $id) {
|
||||
$validated = $request->validate([
|
||||
'deposit' => 'nullable|numeric|min:0',
|
||||
'withdraw' => 'nullable|numeric|min:0',
|
||||
'balance' => 'nullable|numeric',
|
||||
'summary' => 'nullable|string|max:500',
|
||||
'cast' => 'nullable|string|max:100',
|
||||
'memo' => 'nullable|string|max:500',
|
||||
'account_code' => 'nullable|string|max:20',
|
||||
'account_name' => 'nullable|string|max:100',
|
||||
'client_code' => 'nullable|string|max:20',
|
||||
'client_name' => 'nullable|string|max:200',
|
||||
]);
|
||||
|
||||
return $this->service->updateManual($id, $validated);
|
||||
}, __('message.updated'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 수동 은행 거래 삭제
|
||||
*/
|
||||
public function destroyManual(int $id): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id) {
|
||||
return $this->service->destroyManual($id);
|
||||
}, __('message.deleted'));
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 분개 (Journal Entries)
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 은행 거래 분개 조회
|
||||
*/
|
||||
public function getJournalEntries(int $id): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id) {
|
||||
$sourceKey = "barobill_bank_{$id}";
|
||||
|
||||
return $this->journalSyncService->getForSource(
|
||||
JournalEntry::SOURCE_BAROBILL_BANK,
|
||||
$sourceKey
|
||||
) ?? ['items' => []];
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 은행 거래 분개 저장
|
||||
*/
|
||||
public function storeJournalEntries(Request $request, int $id): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request, $id) {
|
||||
$validated = $request->validate([
|
||||
'items' => 'required|array|min:1',
|
||||
'items.*.side' => 'required|in:debit,credit',
|
||||
'items.*.account_code' => 'required|string|max:20',
|
||||
'items.*.account_name' => 'nullable|string|max:100',
|
||||
'items.*.debit_amount' => 'required|integer|min:0',
|
||||
'items.*.credit_amount' => 'required|integer|min:0',
|
||||
'items.*.vendor_name' => 'nullable|string|max:200',
|
||||
'items.*.memo' => 'nullable|string|max:500',
|
||||
]);
|
||||
|
||||
$bankTx = \App\Models\Barobill\BarobillBankTransaction::find($id);
|
||||
if (! $bankTx) {
|
||||
throw new \Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
}
|
||||
|
||||
$entryDate = $bankTx->trans_date ?? now()->format('Y-m-d');
|
||||
$sourceKey = "barobill_bank_{$id}";
|
||||
|
||||
return $this->journalSyncService->saveForSource(
|
||||
JournalEntry::SOURCE_BAROBILL_BANK,
|
||||
$sourceKey,
|
||||
$entryDate,
|
||||
"바로빌 은행거래 분개 (#{$id})",
|
||||
$validated['items'],
|
||||
);
|
||||
}, __('message.created'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 은행 거래 분개 삭제
|
||||
*/
|
||||
public function deleteJournalEntries(int $id): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id) {
|
||||
$sourceKey = "barobill_bank_{$id}";
|
||||
|
||||
return $this->journalSyncService->deleteForSource(
|
||||
JournalEntry::SOURCE_BAROBILL_BANK,
|
||||
$sourceKey
|
||||
);
|
||||
}, __('message.deleted'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,326 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use App\Helpers\ApiResponse;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Tenants\JournalEntry;
|
||||
use App\Services\BarobillCardTransactionService;
|
||||
use App\Services\JournalSyncService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
/**
|
||||
* 바로빌 카드 거래 API 컨트롤러 (React 연동용)
|
||||
*
|
||||
* MNG에서 동기화된 카드 거래 데이터를 React에서 조회/관리
|
||||
*/
|
||||
class BarobillCardTransactionController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
protected BarobillCardTransactionService $service,
|
||||
protected JournalSyncService $journalSyncService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 카드 거래 목록 조회
|
||||
*/
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
$params = $request->validate([
|
||||
'start_date' => 'nullable|date',
|
||||
'end_date' => 'nullable|date|after_or_equal:start_date',
|
||||
'card_num' => 'nullable|string|max:50',
|
||||
'search' => 'nullable|string|max:100',
|
||||
'include_hidden' => 'nullable|boolean',
|
||||
'per_page' => 'nullable|integer|min:1|max:100',
|
||||
'page' => 'nullable|integer|min:1',
|
||||
]);
|
||||
|
||||
return $this->service->index($params);
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 단일 카드 거래 상세
|
||||
*/
|
||||
public function show(int $id): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id) {
|
||||
$tx = $this->service->show($id);
|
||||
if (! $tx) {
|
||||
throw new \Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
}
|
||||
|
||||
return $tx;
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 카드 번호 목록 (필터용)
|
||||
*/
|
||||
public function cardNumbers(): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () {
|
||||
return $this->service->cardNumbers();
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 분할 (Splits)
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 카드 거래 분할 조회
|
||||
*/
|
||||
public function getSplits(Request $request): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
$validated = $request->validate([
|
||||
'unique_key' => 'required|string|max:500',
|
||||
]);
|
||||
|
||||
return $this->service->getSplits($validated['unique_key']);
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 카드 거래 분할 저장
|
||||
*/
|
||||
public function saveSplits(Request $request): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
$validated = $request->validate([
|
||||
'unique_key' => 'required|string|max:500',
|
||||
'items' => 'required|array|min:1',
|
||||
'items.*.split_amount' => 'required|numeric',
|
||||
'items.*.split_supply_amount' => 'nullable|numeric',
|
||||
'items.*.split_tax' => 'nullable|numeric',
|
||||
'items.*.account_code' => 'nullable|string|max:20',
|
||||
'items.*.account_name' => 'nullable|string|max:100',
|
||||
'items.*.deduction_type' => 'nullable|string|max:20',
|
||||
'items.*.evidence_name' => 'nullable|string|max:100',
|
||||
'items.*.description' => 'nullable|string|max:500',
|
||||
'items.*.memo' => 'nullable|string|max:500',
|
||||
'items.*.card_num' => 'nullable|string|max:50',
|
||||
'items.*.use_dt' => 'nullable|string|max:20',
|
||||
'items.*.use_date' => 'nullable|date',
|
||||
'items.*.approval_num' => 'nullable|string|max:50',
|
||||
'items.*.original_amount' => 'nullable|numeric',
|
||||
'items.*.merchant_name' => 'nullable|string|max:200',
|
||||
]);
|
||||
|
||||
return $this->service->saveSplits($validated['unique_key'], $validated['items']);
|
||||
}, __('message.created'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 카드 거래 분할 삭제
|
||||
*/
|
||||
public function deleteSplits(Request $request): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
$validated = $request->validate([
|
||||
'unique_key' => 'required|string|max:500',
|
||||
]);
|
||||
|
||||
return $this->service->deleteSplits($validated['unique_key']);
|
||||
}, __('message.deleted'));
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 수동 입력 (Manual)
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 수동 카드 거래 등록
|
||||
*/
|
||||
public function storeManual(Request $request): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
$validated = $request->validate([
|
||||
'card_num' => 'required|string|max:50',
|
||||
'card_company' => 'nullable|string|max:10',
|
||||
'card_company_name' => 'nullable|string|max:50',
|
||||
'use_dt' => 'required|string|max:20',
|
||||
'use_date' => 'required|date',
|
||||
'use_time' => 'nullable|string|max:10',
|
||||
'approval_num' => 'nullable|string|max:50',
|
||||
'approval_type' => 'nullable|string|max:10',
|
||||
'approval_amount' => 'required|numeric',
|
||||
'tax' => 'nullable|numeric',
|
||||
'service_charge' => 'nullable|numeric',
|
||||
'merchant_name' => 'required|string|max:200',
|
||||
'merchant_biz_num' => 'nullable|string|max:20',
|
||||
'account_code' => 'nullable|string|max:20',
|
||||
'account_name' => 'nullable|string|max:100',
|
||||
'deduction_type' => 'nullable|string|max:20',
|
||||
'evidence_name' => 'nullable|string|max:100',
|
||||
'description' => 'nullable|string|max:500',
|
||||
'memo' => 'nullable|string|max:500',
|
||||
]);
|
||||
|
||||
return $this->service->storeManual($validated);
|
||||
}, __('message.created'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 수동 카드 거래 수정
|
||||
*/
|
||||
public function updateManual(Request $request, int $id): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request, $id) {
|
||||
$validated = $request->validate([
|
||||
'approval_amount' => 'nullable|numeric',
|
||||
'tax' => 'nullable|numeric',
|
||||
'merchant_name' => 'nullable|string|max:200',
|
||||
'account_code' => 'nullable|string|max:20',
|
||||
'account_name' => 'nullable|string|max:100',
|
||||
'deduction_type' => 'nullable|string|max:20',
|
||||
'description' => 'nullable|string|max:500',
|
||||
'memo' => 'nullable|string|max:500',
|
||||
]);
|
||||
|
||||
return $this->service->updateManual($id, $validated);
|
||||
}, __('message.updated'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 수동 카드 거래 삭제
|
||||
*/
|
||||
public function destroyManual(int $id): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id) {
|
||||
return $this->service->destroyManual($id);
|
||||
}, __('message.deleted'));
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 숨김/복원 (Hide/Restore)
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 카드 거래 숨김
|
||||
*/
|
||||
public function hide(int $id): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id) {
|
||||
return $this->service->hide($id);
|
||||
}, __('message.updated'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 카드 거래 숨김 복원
|
||||
*/
|
||||
public function restore(int $id): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id) {
|
||||
return $this->service->restore($id);
|
||||
}, __('message.updated'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 숨겨진 거래 목록
|
||||
*/
|
||||
public function hiddenList(Request $request): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
$params = $request->validate([
|
||||
'start_date' => 'nullable|date',
|
||||
'end_date' => 'nullable|date|after_or_equal:start_date',
|
||||
]);
|
||||
|
||||
return $this->service->hiddenList($params);
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 금액 수정
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 공급가액/세액 수정
|
||||
*/
|
||||
public function updateAmount(Request $request, int $id): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request, $id) {
|
||||
$validated = $request->validate([
|
||||
'supply_amount' => 'required|numeric',
|
||||
'tax' => 'required|numeric',
|
||||
'modified_by_name' => 'nullable|string|max:50',
|
||||
]);
|
||||
|
||||
return $this->service->updateAmount($id, $validated);
|
||||
}, __('message.updated'));
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 분개 (Journal Entries)
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 카드 거래 분개 조회
|
||||
*/
|
||||
public function getJournalEntries(int $id): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id) {
|
||||
$sourceKey = "barobill_card_{$id}";
|
||||
|
||||
return $this->journalSyncService->getForSource(
|
||||
JournalEntry::SOURCE_BAROBILL_CARD,
|
||||
$sourceKey
|
||||
) ?? ['items' => []];
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 카드 거래 분개 저장
|
||||
*/
|
||||
public function storeJournalEntries(Request $request, int $id): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request, $id) {
|
||||
$validated = $request->validate([
|
||||
'items' => 'required|array|min:1',
|
||||
'items.*.side' => 'required|in:debit,credit',
|
||||
'items.*.account_code' => 'required|string|max:20',
|
||||
'items.*.account_name' => 'nullable|string|max:100',
|
||||
'items.*.debit_amount' => 'required|integer|min:0',
|
||||
'items.*.credit_amount' => 'required|integer|min:0',
|
||||
'items.*.vendor_name' => 'nullable|string|max:200',
|
||||
'items.*.memo' => 'nullable|string|max:500',
|
||||
]);
|
||||
|
||||
$tx = $this->service->show($id);
|
||||
if (! $tx) {
|
||||
throw new \Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
}
|
||||
|
||||
$entryDate = $tx->use_date ?? now()->format('Y-m-d');
|
||||
$sourceKey = "barobill_card_{$id}";
|
||||
|
||||
return $this->journalSyncService->saveForSource(
|
||||
JournalEntry::SOURCE_BAROBILL_CARD,
|
||||
$sourceKey,
|
||||
$entryDate,
|
||||
"바로빌 카드거래 분개 (#{$id})",
|
||||
$validated['items'],
|
||||
);
|
||||
}, __('message.created'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 카드 거래 분개 삭제
|
||||
*/
|
||||
public function deleteJournalEntries(int $id): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id) {
|
||||
$sourceKey = "barobill_card_{$id}";
|
||||
|
||||
return $this->journalSyncService->deleteForSource(
|
||||
JournalEntry::SOURCE_BAROBILL_CARD,
|
||||
$sourceKey
|
||||
);
|
||||
}, __('message.deleted'));
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,7 @@ public function status()
|
||||
'barobill_id' => $setting->barobill_id,
|
||||
'biz_no' => $setting->corp_num,
|
||||
'status' => $setting->isVerified() ? 'active' : 'inactive',
|
||||
'server_mode' => config('services.barobill.test_mode', true) ? 'test' : 'production',
|
||||
'server_mode' => $this->barobillService->isTestMode() ? 'test' : 'production',
|
||||
] : null,
|
||||
];
|
||||
}, __('message.fetched'));
|
||||
@@ -86,17 +86,21 @@ public function signup(Request $request)
|
||||
}, __('message.saved'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 바로빌 서비스 URL 조회 (공통)
|
||||
*/
|
||||
private function getServiceUrl(string $path): array
|
||||
{
|
||||
return ['url' => $this->barobillService->getBaseUrl().$path];
|
||||
}
|
||||
|
||||
/**
|
||||
* 은행 빠른조회 서비스 URL 조회
|
||||
*/
|
||||
public function bankServiceUrl(Request $request)
|
||||
public function bankServiceUrl()
|
||||
{
|
||||
return ApiResponse::handle(function () {
|
||||
$baseUrl = config('services.barobill.test_mode', true)
|
||||
? 'https://testws.barobill.co.kr'
|
||||
: 'https://ws.barobill.co.kr';
|
||||
|
||||
return ['url' => $baseUrl.'/Bank/BankAccountService'];
|
||||
return $this->getServiceUrl('/BANKACCOUNT.asmx');
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
@@ -106,11 +110,7 @@ public function bankServiceUrl(Request $request)
|
||||
public function accountLinkUrl()
|
||||
{
|
||||
return ApiResponse::handle(function () {
|
||||
$baseUrl = config('services.barobill.test_mode', true)
|
||||
? 'https://testws.barobill.co.kr'
|
||||
: 'https://ws.barobill.co.kr';
|
||||
|
||||
return ['url' => $baseUrl.'/Bank/AccountLink'];
|
||||
return $this->getServiceUrl('/BANKACCOUNT.asmx');
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
@@ -120,11 +120,7 @@ public function accountLinkUrl()
|
||||
public function cardLinkUrl()
|
||||
{
|
||||
return ApiResponse::handle(function () {
|
||||
$baseUrl = config('services.barobill.test_mode', true)
|
||||
? 'https://testws.barobill.co.kr'
|
||||
: 'https://ws.barobill.co.kr';
|
||||
|
||||
return ['url' => $baseUrl.'/Card/CardLink'];
|
||||
return $this->getServiceUrl('/CARD.asmx');
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
@@ -134,11 +130,7 @@ public function cardLinkUrl()
|
||||
public function certificateUrl()
|
||||
{
|
||||
return ApiResponse::handle(function () {
|
||||
$baseUrl = config('services.barobill.test_mode', true)
|
||||
? 'https://testws.barobill.co.kr'
|
||||
: 'https://ws.barobill.co.kr';
|
||||
|
||||
return ['url' => $baseUrl.'/Certificate/Register'];
|
||||
return $this->getServiceUrl('/CORPSTATE.asmx');
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,12 +18,9 @@ public function __construct(
|
||||
*/
|
||||
public function show()
|
||||
{
|
||||
$setting = $this->barobillService->getSetting();
|
||||
|
||||
return ApiResponse::handle(
|
||||
data: $setting,
|
||||
message: __('message.fetched')
|
||||
);
|
||||
return ApiResponse::handle(function () {
|
||||
return $this->barobillService->getSetting();
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -31,12 +28,9 @@ public function show()
|
||||
*/
|
||||
public function save(SaveBarobillSettingRequest $request)
|
||||
{
|
||||
$setting = $this->barobillService->saveSetting($request->validated());
|
||||
|
||||
return ApiResponse::handle(
|
||||
data: $setting,
|
||||
message: __('message.saved')
|
||||
);
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
return $this->barobillService->saveSetting($request->validated());
|
||||
}, __('message.saved'));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -44,11 +38,8 @@ public function save(SaveBarobillSettingRequest $request)
|
||||
*/
|
||||
public function testConnection()
|
||||
{
|
||||
$result = $this->barobillService->testConnection();
|
||||
|
||||
return ApiResponse::handle(
|
||||
data: $result,
|
||||
message: __('message.barobill.connection_success')
|
||||
);
|
||||
return ApiResponse::handle(function () {
|
||||
return $this->barobillService->testConnection();
|
||||
}, __('message.barobill.connection_success'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
|
||||
use App\Helpers\ApiResponse;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Tenants\JournalEntry;
|
||||
use App\Services\CardTransactionService;
|
||||
use App\Services\JournalSyncService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
@@ -14,7 +16,8 @@
|
||||
class CardTransactionController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
protected CardTransactionService $service
|
||||
protected CardTransactionService $service,
|
||||
protected JournalSyncService $journalSyncService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -148,4 +151,105 @@ public function destroy(int $id): JsonResponse
|
||||
return $this->service->destroy($id);
|
||||
}, __('message.deleted'));
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 분개 (Journal Entries)
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 카드 거래 분개 조회
|
||||
*/
|
||||
public function getJournalEntries(int $id): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id) {
|
||||
$sourceKey = "card_{$id}";
|
||||
$data = $this->journalSyncService->getForSource(
|
||||
JournalEntry::SOURCE_CARD_TRANSACTION,
|
||||
$sourceKey
|
||||
);
|
||||
|
||||
if (! $data) {
|
||||
return ['items' => []];
|
||||
}
|
||||
|
||||
// 프론트엔드가 기대하는 items 형식으로 변환
|
||||
$items = array_map(fn ($row) => [
|
||||
'id' => $row['id'],
|
||||
'supply_amount' => $row['debit_amount'],
|
||||
'tax_amount' => 0,
|
||||
'account_code' => $row['account_code'],
|
||||
'deduction_type' => 'deductible',
|
||||
'vendor_name' => $row['vendor_name'],
|
||||
'description' => $row['memo'],
|
||||
'memo' => '',
|
||||
], $data['rows']);
|
||||
|
||||
return ['items' => $items];
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 카드 거래 분개 저장
|
||||
*/
|
||||
public function storeJournalEntries(Request $request, int $id): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request, $id) {
|
||||
$validated = $request->validate([
|
||||
'items' => 'required|array|min:1',
|
||||
'items.*.supply_amount' => 'required|integer|min:0',
|
||||
'items.*.tax_amount' => 'required|integer|min:0',
|
||||
'items.*.account_code' => 'required|string|max:20',
|
||||
'items.*.deduction_type' => 'nullable|string|max:20',
|
||||
'items.*.vendor_name' => 'nullable|string|max:200',
|
||||
'items.*.description' => 'nullable|string|max:500',
|
||||
'items.*.memo' => 'nullable|string|max:500',
|
||||
]);
|
||||
|
||||
// 카드 거래 정보 조회 (날짜용)
|
||||
$transaction = $this->service->show($id);
|
||||
if (! $transaction) {
|
||||
throw new \Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
}
|
||||
|
||||
$entryDate = $transaction->used_at
|
||||
? $transaction->used_at->format('Y-m-d')
|
||||
: ($transaction->withdrawal_date?->format('Y-m-d') ?? now()->format('Y-m-d'));
|
||||
|
||||
// items → journal rows 변환 (각 item을 차변 행으로)
|
||||
$rows = [];
|
||||
foreach ($validated['items'] as $item) {
|
||||
$amount = ($item['supply_amount'] ?? 0) + ($item['tax_amount'] ?? 0);
|
||||
$rows[] = [
|
||||
'side' => 'debit',
|
||||
'account_code' => $item['account_code'],
|
||||
'debit_amount' => $amount,
|
||||
'credit_amount' => 0,
|
||||
'vendor_name' => $item['vendor_name'] ?? '',
|
||||
'memo' => $item['description'] ?? $item['memo'] ?? '',
|
||||
];
|
||||
}
|
||||
|
||||
// 대변 합계 행 (카드미지급금)
|
||||
$totalAmount = array_sum(array_column($rows, 'debit_amount'));
|
||||
$rows[] = [
|
||||
'side' => 'credit',
|
||||
'account_code' => '25300', // 미지급금 (표준 코드)
|
||||
'account_name' => '미지급금',
|
||||
'debit_amount' => 0,
|
||||
'credit_amount' => $totalAmount,
|
||||
'vendor_name' => $transaction->merchant_name ?? '',
|
||||
'memo' => '카드결제',
|
||||
];
|
||||
|
||||
$sourceKey = "card_{$id}";
|
||||
|
||||
return $this->journalSyncService->saveForSource(
|
||||
JournalEntry::SOURCE_CARD_TRANSACTION,
|
||||
$sourceKey,
|
||||
$entryDate,
|
||||
"카드거래 분개 (#{$id})",
|
||||
$rows,
|
||||
);
|
||||
}, __('message.created'));
|
||||
}
|
||||
}
|
||||
|
||||
92
app/Http/Controllers/Api/V1/ChecklistTemplateController.php
Normal file
92
app/Http/Controllers/Api/V1/ChecklistTemplateController.php
Normal file
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use App\Helpers\ApiResponse;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Quality\SaveChecklistTemplateRequest;
|
||||
use App\Services\ChecklistTemplateService;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ChecklistTemplateController extends Controller
|
||||
{
|
||||
public function __construct(private ChecklistTemplateService $service) {}
|
||||
|
||||
/**
|
||||
* 템플릿 조회 (type별)
|
||||
*/
|
||||
public function show(Request $request)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
$type = $request->query('type', 'day1_audit');
|
||||
|
||||
return $this->service->getByType($type);
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 템플릿 저장 (전체 덮어쓰기)
|
||||
*/
|
||||
public function update(SaveChecklistTemplateRequest $request, int $id)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request, $id) {
|
||||
return $this->service->save($id, $request->validated());
|
||||
}, __('message.updated'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 항목 완료 토글
|
||||
*/
|
||||
public function toggleItem(int $id, string $subItemId)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id, $subItemId) {
|
||||
return $this->service->toggleItem($id, $subItemId);
|
||||
}, __('message.updated'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 항목별 파일 목록 조회
|
||||
*/
|
||||
public function documents(Request $request)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
$templateId = (int) $request->query('template_id');
|
||||
$subItemId = $request->query('sub_item_id');
|
||||
|
||||
return $this->service->getDocuments($templateId, $subItemId);
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 파일 업로드
|
||||
*/
|
||||
public function uploadDocument(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'template_id' => ['required', 'integer'],
|
||||
'sub_item_id' => ['required', 'string', 'max:50'],
|
||||
'file' => ['required', 'file', 'max:10240'], // 10MB
|
||||
]);
|
||||
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
return $this->service->uploadDocument(
|
||||
(int) $request->input('template_id'),
|
||||
$request->input('sub_item_id'),
|
||||
$request->file('file')
|
||||
);
|
||||
}, __('message.created'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 파일 삭제
|
||||
*/
|
||||
public function deleteDocument(int $id, Request $request)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id, $request) {
|
||||
$replace = filter_var($request->query('replace', false), FILTER_VALIDATE_BOOLEAN);
|
||||
$this->service->deleteDocument($id, $replace);
|
||||
|
||||
return 'success';
|
||||
}, __('message.deleted'));
|
||||
}
|
||||
}
|
||||
@@ -83,14 +83,25 @@ public function trash()
|
||||
}
|
||||
|
||||
/**
|
||||
* Download file
|
||||
* Download file (attachment)
|
||||
*/
|
||||
public function download(int $id)
|
||||
{
|
||||
$service = new FileStorageService;
|
||||
$file = $service->getFile($id);
|
||||
|
||||
return $file->download();
|
||||
return $file->download(inline: false);
|
||||
}
|
||||
|
||||
/**
|
||||
* View file inline (이미지/PDF 브라우저에서 바로 표시)
|
||||
*/
|
||||
public function view(int $id)
|
||||
{
|
||||
$service = new FileStorageService;
|
||||
$file = $service->getFile($id);
|
||||
|
||||
return $file->download(inline: true);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
278
app/Http/Controllers/Api/V1/HometaxInvoiceController.php
Normal file
278
app/Http/Controllers/Api/V1/HometaxInvoiceController.php
Normal file
@@ -0,0 +1,278 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use App\Helpers\ApiResponse;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Tenants\JournalEntry;
|
||||
use App\Services\HometaxInvoiceService;
|
||||
use App\Services\JournalSyncService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
/**
|
||||
* 홈택스 세금계산서 API 컨트롤러 (React 연동용)
|
||||
*
|
||||
* MNG에서 동기화된 홈택스 세금계산서를 React에서 조회/관리
|
||||
*/
|
||||
class HometaxInvoiceController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
protected HometaxInvoiceService $service,
|
||||
protected JournalSyncService $journalSyncService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 매출 세금계산서 목록
|
||||
*/
|
||||
public function sales(Request $request): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
$params = $request->validate([
|
||||
'start_date' => 'nullable|date',
|
||||
'end_date' => 'nullable|date|after_or_equal:start_date',
|
||||
'search' => 'nullable|string|max:100',
|
||||
'per_page' => 'nullable|integer|min:1|max:100',
|
||||
'page' => 'nullable|integer|min:1',
|
||||
]);
|
||||
|
||||
return $this->service->sales($params);
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 매입 세금계산서 목록
|
||||
*/
|
||||
public function purchases(Request $request): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
$params = $request->validate([
|
||||
'start_date' => 'nullable|date',
|
||||
'end_date' => 'nullable|date|after_or_equal:start_date',
|
||||
'search' => 'nullable|string|max:100',
|
||||
'per_page' => 'nullable|integer|min:1|max:100',
|
||||
'page' => 'nullable|integer|min:1',
|
||||
]);
|
||||
|
||||
return $this->service->purchases($params);
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 세금계산서 상세 조회
|
||||
*/
|
||||
public function show(int $id): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id) {
|
||||
$invoice = $this->service->show($id);
|
||||
if (! $invoice) {
|
||||
throw new \Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
}
|
||||
|
||||
return $invoice;
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 요약 통계 (매출/매입 합계)
|
||||
*/
|
||||
public function summary(Request $request): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
$params = $request->validate([
|
||||
'start_date' => 'nullable|date',
|
||||
'end_date' => 'nullable|date|after_or_equal:start_date',
|
||||
]);
|
||||
|
||||
return $this->service->summary($params);
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 수동 입력 (Manual)
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 수동 세금계산서 등록
|
||||
*/
|
||||
public function store(Request $request): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
$validated = $request->validate([
|
||||
'invoice_type' => 'required|in:sales,purchase',
|
||||
'nts_confirm_num' => 'nullable|string|max:50',
|
||||
'write_date' => 'required|date',
|
||||
'issue_date' => 'nullable|date',
|
||||
'invoicer_corp_num' => 'nullable|string|max:20',
|
||||
'invoicer_corp_name' => 'nullable|string|max:200',
|
||||
'invoicer_ceo_name' => 'nullable|string|max:100',
|
||||
'invoicee_corp_num' => 'nullable|string|max:20',
|
||||
'invoicee_corp_name' => 'nullable|string|max:200',
|
||||
'invoicee_ceo_name' => 'nullable|string|max:100',
|
||||
'supply_amount' => 'required|integer',
|
||||
'tax_amount' => 'required|integer',
|
||||
'total_amount' => 'required|integer',
|
||||
'tax_type' => 'nullable|string|max:10',
|
||||
'purpose_type' => 'nullable|string|max:10',
|
||||
'issue_type' => 'nullable|string|max:10',
|
||||
'item_name' => 'nullable|string|max:200',
|
||||
'account_code' => 'nullable|string|max:20',
|
||||
'account_name' => 'nullable|string|max:100',
|
||||
'deduction_type' => 'nullable|string|max:20',
|
||||
'remark1' => 'nullable|string|max:500',
|
||||
]);
|
||||
|
||||
return $this->service->storeManual($validated);
|
||||
}, __('message.created'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 수동 세금계산서 수정
|
||||
*/
|
||||
public function update(Request $request, int $id): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request, $id) {
|
||||
$validated = $request->validate([
|
||||
'write_date' => 'nullable|date',
|
||||
'issue_date' => 'nullable|date',
|
||||
'invoicer_corp_num' => 'nullable|string|max:20',
|
||||
'invoicer_corp_name' => 'nullable|string|max:200',
|
||||
'invoicee_corp_num' => 'nullable|string|max:20',
|
||||
'invoicee_corp_name' => 'nullable|string|max:200',
|
||||
'supply_amount' => 'nullable|integer',
|
||||
'tax_amount' => 'nullable|integer',
|
||||
'total_amount' => 'nullable|integer',
|
||||
'item_name' => 'nullable|string|max:200',
|
||||
'account_code' => 'nullable|string|max:20',
|
||||
'account_name' => 'nullable|string|max:100',
|
||||
'deduction_type' => 'nullable|string|max:20',
|
||||
'remark1' => 'nullable|string|max:500',
|
||||
]);
|
||||
|
||||
return $this->service->updateManual($id, $validated);
|
||||
}, __('message.updated'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 수동 세금계산서 삭제
|
||||
*/
|
||||
public function destroy(int $id): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id) {
|
||||
return $this->service->destroyManual($id);
|
||||
}, __('message.deleted'));
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 분개 (자체 테이블: hometax_invoice_journals)
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 분개 조회
|
||||
*/
|
||||
public function getJournals(int $id): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id) {
|
||||
return $this->service->getJournals($id);
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 분개 저장
|
||||
*/
|
||||
public function saveJournals(Request $request, int $id): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request, $id) {
|
||||
$validated = $request->validate([
|
||||
'items' => 'required|array|min:1',
|
||||
'items.*.dc_type' => 'required|in:debit,credit',
|
||||
'items.*.account_code' => 'required|string|max:20',
|
||||
'items.*.account_name' => 'nullable|string|max:100',
|
||||
'items.*.debit_amount' => 'required|integer|min:0',
|
||||
'items.*.credit_amount' => 'required|integer|min:0',
|
||||
'items.*.description' => 'nullable|string|max:500',
|
||||
]);
|
||||
|
||||
return $this->service->saveJournals($id, $validated['items']);
|
||||
}, __('message.created'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 분개 삭제
|
||||
*/
|
||||
public function deleteJournals(int $id): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id) {
|
||||
return $this->service->deleteJournals($id);
|
||||
}, __('message.deleted'));
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 통합 분개 (JournalSyncService - CEO 대시보드 연동)
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 통합 분개 조회
|
||||
*/
|
||||
public function getJournalEntries(int $id): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id) {
|
||||
$sourceKey = "hometax_{$id}";
|
||||
|
||||
return $this->journalSyncService->getForSource(
|
||||
JournalEntry::SOURCE_HOMETAX_INVOICE,
|
||||
$sourceKey
|
||||
) ?? ['items' => []];
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 통합 분개 저장
|
||||
*/
|
||||
public function storeJournalEntries(Request $request, int $id): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request, $id) {
|
||||
$validated = $request->validate([
|
||||
'items' => 'required|array|min:1',
|
||||
'items.*.side' => 'required|in:debit,credit',
|
||||
'items.*.account_code' => 'required|string|max:20',
|
||||
'items.*.account_name' => 'nullable|string|max:100',
|
||||
'items.*.debit_amount' => 'required|integer|min:0',
|
||||
'items.*.credit_amount' => 'required|integer|min:0',
|
||||
'items.*.vendor_name' => 'nullable|string|max:200',
|
||||
'items.*.memo' => 'nullable|string|max:500',
|
||||
]);
|
||||
|
||||
$invoice = $this->service->show($id);
|
||||
if (! $invoice) {
|
||||
throw new \Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
}
|
||||
|
||||
$entryDate = $invoice->write_date?->format('Y-m-d') ?? now()->format('Y-m-d');
|
||||
$sourceKey = "hometax_{$id}";
|
||||
|
||||
return $this->journalSyncService->saveForSource(
|
||||
JournalEntry::SOURCE_HOMETAX_INVOICE,
|
||||
$sourceKey,
|
||||
$entryDate,
|
||||
"홈택스 세금계산서 분개 (#{$id})",
|
||||
$validated['items'],
|
||||
);
|
||||
}, __('message.created'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 통합 분개 삭제
|
||||
*/
|
||||
public function deleteJournalEntries(int $id): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id) {
|
||||
$sourceKey = "hometax_{$id}";
|
||||
|
||||
return $this->journalSyncService->deleteForSource(
|
||||
JournalEntry::SOURCE_HOMETAX_INVOICE,
|
||||
$sourceKey
|
||||
);
|
||||
}, __('message.deleted'));
|
||||
}
|
||||
}
|
||||
@@ -109,7 +109,7 @@ public function upload(int $id, ItemFileUploadRequest $request)
|
||||
$filePath = $directory.'/'.$storedName;
|
||||
|
||||
// 파일 저장 (tenant 디스크)
|
||||
Storage::disk('tenant')->putFileAs($directory, $uploadedFile, $storedName);
|
||||
Storage::disk('r2')->putFileAs($directory, $uploadedFile, $storedName);
|
||||
|
||||
// file_type 자동 분류 (MIME 타입 기반)
|
||||
$mimeType = $uploadedFile->getMimeType();
|
||||
|
||||
@@ -4,18 +4,24 @@
|
||||
|
||||
use App\Helpers\ApiResponse;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\V1\Payroll\BulkGeneratePayrollRequest;
|
||||
use App\Http\Requests\V1\Payroll\CalculatePayrollRequest;
|
||||
use App\Http\Requests\V1\Payroll\CopyFromPreviousPayrollRequest;
|
||||
use App\Http\Requests\V1\Payroll\PayPayrollRequest;
|
||||
use App\Http\Requests\V1\Payroll\StorePayrollJournalRequest;
|
||||
use App\Http\Requests\V1\Payroll\StorePayrollRequest;
|
||||
use App\Http\Requests\V1\Payroll\UpdatePayrollRequest;
|
||||
use App\Http\Requests\V1\Payroll\UpdatePayrollSettingRequest;
|
||||
use App\Services\ExportService;
|
||||
use App\Services\PayrollService;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
||||
|
||||
class PayrollController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly PayrollService $service
|
||||
private readonly PayrollService $service,
|
||||
private readonly ExportService $exportService
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -28,6 +34,7 @@ public function index(Request $request)
|
||||
'month',
|
||||
'user_id',
|
||||
'status',
|
||||
'department_id',
|
||||
'search',
|
||||
'sort_by',
|
||||
'sort_dir',
|
||||
@@ -103,6 +110,16 @@ public function confirm(int $id)
|
||||
return ApiResponse::success($payroll, __('message.payroll.confirmed'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 급여 확정 취소
|
||||
*/
|
||||
public function unconfirm(int $id)
|
||||
{
|
||||
$payroll = $this->service->unconfirm($id);
|
||||
|
||||
return ApiResponse::success($payroll, __('message.payroll.unconfirmed'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 급여 지급 처리
|
||||
*/
|
||||
@@ -113,6 +130,16 @@ public function pay(int $id, PayPayrollRequest $request)
|
||||
return ApiResponse::success($payroll, __('message.payroll.paid'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 급여 지급 취소 (슈퍼관리자)
|
||||
*/
|
||||
public function unpay(int $id)
|
||||
{
|
||||
$payroll = $this->service->unpay($id);
|
||||
|
||||
return ApiResponse::success($payroll, __('message.payroll.unpaid'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 일괄 확정
|
||||
*/
|
||||
@@ -127,13 +154,29 @@ public function bulkConfirm(Request $request)
|
||||
}
|
||||
|
||||
/**
|
||||
* 급여명세서 조회
|
||||
* 재직사원 일괄 생성
|
||||
*/
|
||||
public function payslip(int $id)
|
||||
public function bulkGenerate(BulkGeneratePayrollRequest $request)
|
||||
{
|
||||
$payslip = $this->service->payslip($id);
|
||||
$year = (int) $request->input('year');
|
||||
$month = (int) $request->input('month');
|
||||
|
||||
return ApiResponse::success($payslip, __('message.fetched'));
|
||||
$result = $this->service->bulkGenerate($year, $month);
|
||||
|
||||
return ApiResponse::success($result, __('message.payroll.bulk_generated'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 전월 급여 복사
|
||||
*/
|
||||
public function copyFromPrevious(CopyFromPreviousPayrollRequest $request)
|
||||
{
|
||||
$year = (int) $request->input('year');
|
||||
$month = (int) $request->input('month');
|
||||
|
||||
$result = $this->service->copyFromPreviousMonth($year, $month);
|
||||
|
||||
return ApiResponse::success($result, __('message.payroll.copied'));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -150,6 +193,76 @@ public function calculate(CalculatePayrollRequest $request)
|
||||
return ApiResponse::success($payrolls, __('message.payroll.calculated'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 급여 계산 미리보기
|
||||
*/
|
||||
public function calculatePreview(Request $request)
|
||||
{
|
||||
$data = $request->only([
|
||||
'user_id',
|
||||
'base_salary',
|
||||
'overtime_pay',
|
||||
'bonus',
|
||||
'allowances',
|
||||
'deductions',
|
||||
]);
|
||||
|
||||
$result = $this->service->calculatePreview($data);
|
||||
|
||||
return ApiResponse::success($result, __('message.calculated'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 급여명세서 조회
|
||||
*/
|
||||
public function payslip(int $id)
|
||||
{
|
||||
$payslip = $this->service->payslip($id);
|
||||
|
||||
return ApiResponse::success($payslip, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 급여 엑셀 내보내기
|
||||
*/
|
||||
public function export(Request $request): BinaryFileResponse
|
||||
{
|
||||
$params = $request->only([
|
||||
'year',
|
||||
'month',
|
||||
'status',
|
||||
'user_id',
|
||||
'department_id',
|
||||
'search',
|
||||
'sort_by',
|
||||
'sort_dir',
|
||||
]);
|
||||
|
||||
$exportData = $this->service->getExportData($params);
|
||||
$filename = '급여현황_'.date('Ymd_His');
|
||||
|
||||
return $this->exportService->download(
|
||||
$exportData['data'],
|
||||
$exportData['headings'],
|
||||
$filename,
|
||||
'급여현황'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 급여 전표 생성
|
||||
*/
|
||||
public function journalEntries(StorePayrollJournalRequest $request)
|
||||
{
|
||||
$year = (int) $request->input('year');
|
||||
$month = (int) $request->input('month');
|
||||
$entryDate = $request->input('entry_date');
|
||||
|
||||
$entry = $this->service->createJournalEntries($year, $month, $entryDate);
|
||||
|
||||
return ApiResponse::success($entry, __('message.payroll.journal_created'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 급여 설정 조회
|
||||
*/
|
||||
|
||||
65
app/Http/Controllers/Api/V1/QmsLotAuditController.php
Normal file
65
app/Http/Controllers/Api/V1/QmsLotAuditController.php
Normal file
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use App\Helpers\ApiResponse;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Qms\QmsLotAuditConfirmRequest;
|
||||
use App\Http\Requests\Qms\QmsLotAuditDocumentDetailRequest;
|
||||
use App\Http\Requests\Qms\QmsLotAuditIndexRequest;
|
||||
use App\Services\QmsLotAuditService;
|
||||
|
||||
class QmsLotAuditController extends Controller
|
||||
{
|
||||
public function __construct(private QmsLotAuditService $service) {}
|
||||
|
||||
/**
|
||||
* 품질관리서 목록 (로트 추적 심사용)
|
||||
*/
|
||||
public function index(QmsLotAuditIndexRequest $request)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
return $this->service->index($request->validated());
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 품질관리서 상세 — 수주/개소 목록
|
||||
*/
|
||||
public function show(int $id)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id) {
|
||||
return $this->service->show($id);
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 수주 루트별 8종 서류 목록
|
||||
*/
|
||||
public function routeDocuments(int $id)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id) {
|
||||
return $this->service->routeDocuments($id);
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 서류 상세 조회 (2단계 로딩)
|
||||
*/
|
||||
public function documentDetail(QmsLotAuditDocumentDetailRequest $request, string $type, int $id)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($type, $id) {
|
||||
return $this->service->documentDetail($type, $id);
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 개소별 로트 심사 확인 토글
|
||||
*/
|
||||
public function confirm(QmsLotAuditConfirmRequest $request, int $id)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request, $id) {
|
||||
return $this->service->confirm($id, $request->validated());
|
||||
}, __('message.updated'));
|
||||
}
|
||||
}
|
||||
@@ -8,13 +8,15 @@
|
||||
use App\Http\Requests\Shipment\ShipmentUpdateRequest;
|
||||
use App\Http\Requests\Shipment\ShipmentUpdateStatusRequest;
|
||||
use App\Services\ShipmentService;
|
||||
use App\Services\WorkOrderService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ShipmentController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ShipmentService $service
|
||||
private readonly ShipmentService $service,
|
||||
private readonly WorkOrderService $workOrderService
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -83,7 +85,7 @@ public function store(ShipmentStoreRequest $request): JsonResponse
|
||||
{
|
||||
$shipment = $this->service->store($request->validated());
|
||||
|
||||
return ApiResponse::success($shipment, __('message.created'), 201);
|
||||
return ApiResponse::success($shipment, __('message.created'), [], 201);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -132,6 +134,22 @@ public function destroy(int $id): JsonResponse
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 수주 기반 출하 생성
|
||||
*/
|
||||
public function createFromOrder(int $orderId): JsonResponse
|
||||
{
|
||||
try {
|
||||
$shipment = $this->workOrderService->createShipmentForOrder($orderId);
|
||||
|
||||
return ApiResponse::success($shipment, __('message.created'), [], 201);
|
||||
} catch (\Symfony\Component\HttpKernel\Exception\BadRequestHttpException $e) {
|
||||
return ApiResponse::error($e->getMessage(), 400);
|
||||
} catch (\Illuminate\Database\Eloquent\ModelNotFoundException $e) {
|
||||
return ApiResponse::error(__('error.order.not_found'), 404);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* LOT 옵션 조회
|
||||
*/
|
||||
|
||||
@@ -10,12 +10,17 @@
|
||||
use App\Http\Requests\TaxInvoice\TaxInvoiceSummaryRequest;
|
||||
use App\Http\Requests\TaxInvoice\UpdateTaxInvoiceRequest;
|
||||
use App\Http\Requests\V1\TaxInvoice\BulkIssueRequest;
|
||||
use App\Models\Tenants\JournalEntry;
|
||||
use App\Services\JournalSyncService;
|
||||
use App\Services\TaxInvoiceService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class TaxInvoiceController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private TaxInvoiceService $taxInvoiceService
|
||||
private TaxInvoiceService $taxInvoiceService,
|
||||
private JournalSyncService $journalSyncService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -23,12 +28,9 @@ public function __construct(
|
||||
*/
|
||||
public function index(TaxInvoiceListRequest $request)
|
||||
{
|
||||
$taxInvoices = $this->taxInvoiceService->list($request->validated());
|
||||
|
||||
return ApiResponse::handle(
|
||||
data: $taxInvoices,
|
||||
message: __('message.fetched')
|
||||
);
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
return $this->taxInvoiceService->list($request->validated());
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -36,12 +38,9 @@ public function index(TaxInvoiceListRequest $request)
|
||||
*/
|
||||
public function show(int $id)
|
||||
{
|
||||
$taxInvoice = $this->taxInvoiceService->show($id);
|
||||
|
||||
return ApiResponse::handle(
|
||||
data: $taxInvoice,
|
||||
message: __('message.fetched')
|
||||
);
|
||||
return ApiResponse::handle(function () use ($id) {
|
||||
return $this->taxInvoiceService->show($id);
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -49,13 +48,9 @@ public function show(int $id)
|
||||
*/
|
||||
public function store(CreateTaxInvoiceRequest $request)
|
||||
{
|
||||
$taxInvoice = $this->taxInvoiceService->create($request->validated());
|
||||
|
||||
return ApiResponse::handle(
|
||||
data: $taxInvoice,
|
||||
message: __('message.created'),
|
||||
status: 201
|
||||
);
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
return $this->taxInvoiceService->create($request->validated());
|
||||
}, __('message.created'));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -63,12 +58,9 @@ public function store(CreateTaxInvoiceRequest $request)
|
||||
*/
|
||||
public function update(UpdateTaxInvoiceRequest $request, int $id)
|
||||
{
|
||||
$taxInvoice = $this->taxInvoiceService->update($id, $request->validated());
|
||||
|
||||
return ApiResponse::handle(
|
||||
data: $taxInvoice,
|
||||
message: __('message.updated')
|
||||
);
|
||||
return ApiResponse::handle(function () use ($request, $id) {
|
||||
return $this->taxInvoiceService->update($id, $request->validated());
|
||||
}, __('message.updated'));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -76,12 +68,11 @@ public function update(UpdateTaxInvoiceRequest $request, int $id)
|
||||
*/
|
||||
public function destroy(int $id)
|
||||
{
|
||||
$this->taxInvoiceService->delete($id);
|
||||
return ApiResponse::handle(function () use ($id) {
|
||||
$this->taxInvoiceService->delete($id);
|
||||
|
||||
return ApiResponse::handle(
|
||||
data: null,
|
||||
message: __('message.deleted')
|
||||
);
|
||||
return null;
|
||||
}, __('message.deleted'));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -89,12 +80,9 @@ public function destroy(int $id)
|
||||
*/
|
||||
public function issue(int $id)
|
||||
{
|
||||
$taxInvoice = $this->taxInvoiceService->issue($id);
|
||||
|
||||
return ApiResponse::handle(
|
||||
data: $taxInvoice,
|
||||
message: __('message.tax_invoice.issued')
|
||||
);
|
||||
return ApiResponse::handle(function () use ($id) {
|
||||
return $this->taxInvoiceService->issue($id);
|
||||
}, __('message.tax_invoice.issued'));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -102,12 +90,9 @@ public function issue(int $id)
|
||||
*/
|
||||
public function bulkIssue(BulkIssueRequest $request)
|
||||
{
|
||||
$result = $this->taxInvoiceService->bulkIssue($request->getIds());
|
||||
|
||||
return ApiResponse::handle(
|
||||
data: $result,
|
||||
message: __('message.tax_invoice.bulk_issued')
|
||||
);
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
return $this->taxInvoiceService->bulkIssue($request->getIds());
|
||||
}, __('message.tax_invoice.bulk_issued'));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -115,12 +100,9 @@ public function bulkIssue(BulkIssueRequest $request)
|
||||
*/
|
||||
public function cancel(CancelTaxInvoiceRequest $request, int $id)
|
||||
{
|
||||
$taxInvoice = $this->taxInvoiceService->cancel($id, $request->validated()['reason']);
|
||||
|
||||
return ApiResponse::handle(
|
||||
data: $taxInvoice,
|
||||
message: __('message.tax_invoice.cancelled')
|
||||
);
|
||||
return ApiResponse::handle(function () use ($request, $id) {
|
||||
return $this->taxInvoiceService->cancel($id, $request->validated()['reason']);
|
||||
}, __('message.tax_invoice.cancelled'));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -128,12 +110,9 @@ public function cancel(CancelTaxInvoiceRequest $request, int $id)
|
||||
*/
|
||||
public function checkStatus(int $id)
|
||||
{
|
||||
$taxInvoice = $this->taxInvoiceService->checkStatus($id);
|
||||
|
||||
return ApiResponse::handle(
|
||||
data: $taxInvoice,
|
||||
message: __('message.fetched')
|
||||
);
|
||||
return ApiResponse::handle(function () use ($id) {
|
||||
return $this->taxInvoiceService->checkStatus($id);
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -141,11 +120,79 @@ public function checkStatus(int $id)
|
||||
*/
|
||||
public function summary(TaxInvoiceSummaryRequest $request)
|
||||
{
|
||||
$summary = $this->taxInvoiceService->summary($request->validated());
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
return $this->taxInvoiceService->summary($request->validated());
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
return ApiResponse::handle(
|
||||
data: $summary,
|
||||
message: __('message.fetched')
|
||||
);
|
||||
// =========================================================================
|
||||
// 분개 (Journal Entries)
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 세금계산서 분개 조회
|
||||
*/
|
||||
public function getJournalEntries(int $id): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id) {
|
||||
$sourceKey = "tax_invoice_{$id}";
|
||||
$data = $this->journalSyncService->getForSource(
|
||||
JournalEntry::SOURCE_TAX_INVOICE,
|
||||
$sourceKey
|
||||
);
|
||||
|
||||
return $data ?? ['rows' => []];
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 세금계산서 분개 저장/수정
|
||||
*/
|
||||
public function storeJournalEntries(Request $request, int $id): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request, $id) {
|
||||
$validated = $request->validate([
|
||||
'rows' => 'required|array|min:1',
|
||||
'rows.*.side' => 'required|in:debit,credit',
|
||||
'rows.*.account_subject' => 'required|string|max:20',
|
||||
'rows.*.debit_amount' => 'required|integer|min:0',
|
||||
'rows.*.credit_amount' => 'required|integer|min:0',
|
||||
]);
|
||||
|
||||
// 세금계산서 정보 조회 (entry_date용)
|
||||
$taxInvoice = $this->taxInvoiceService->show($id);
|
||||
|
||||
$rows = array_map(fn ($row) => [
|
||||
'side' => $row['side'],
|
||||
'account_code' => $row['account_subject'],
|
||||
'debit_amount' => $row['debit_amount'],
|
||||
'credit_amount' => $row['credit_amount'],
|
||||
], $validated['rows']);
|
||||
|
||||
$sourceKey = "tax_invoice_{$id}";
|
||||
|
||||
return $this->journalSyncService->saveForSource(
|
||||
JournalEntry::SOURCE_TAX_INVOICE,
|
||||
$sourceKey,
|
||||
$taxInvoice->issue_date?->format('Y-m-d') ?? now()->format('Y-m-d'),
|
||||
"세금계산서 분개 (#{$id})",
|
||||
$rows,
|
||||
);
|
||||
}, __('message.created'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 세금계산서 분개 삭제
|
||||
*/
|
||||
public function deleteJournalEntries(int $id): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id) {
|
||||
$sourceKey = "tax_invoice_{$id}";
|
||||
|
||||
return $this->journalSyncService->deleteForSource(
|
||||
JournalEntry::SOURCE_TAX_INVOICE,
|
||||
$sourceKey
|
||||
);
|
||||
}, __('message.deleted'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,14 +61,18 @@ public function detail(Request $request): JsonResponse
|
||||
: 0.05;
|
||||
$year = $request->query('year') ? (int) $request->query('year') : null;
|
||||
$quarter = $request->query('quarter') ? (int) $request->query('quarter') : null;
|
||||
$startDate = $request->query('start_date');
|
||||
$endDate = $request->query('end_date');
|
||||
|
||||
return ApiResponse::handle(function () use ($calculationType, $fixedAmountPerMonth, $ratio, $year, $quarter) {
|
||||
return ApiResponse::handle(function () use ($calculationType, $fixedAmountPerMonth, $ratio, $year, $quarter, $startDate, $endDate) {
|
||||
return $this->welfareService->getDetail(
|
||||
$calculationType,
|
||||
$fixedAmountPerMonth,
|
||||
$ratio,
|
||||
$year,
|
||||
$quarter
|
||||
$quarter,
|
||||
$startDate,
|
||||
$endDate
|
||||
);
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
91
app/Http/Controllers/V1/Equipment/EquipmentController.php
Normal file
91
app/Http/Controllers/V1/Equipment/EquipmentController.php
Normal file
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V1\Equipment;
|
||||
|
||||
use App\Helpers\ApiResponse;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\V1\Equipment\StoreEquipmentRequest;
|
||||
use App\Http\Requests\V1\Equipment\UpdateEquipmentRequest;
|
||||
use App\Services\Equipment\EquipmentService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class EquipmentController extends Controller
|
||||
{
|
||||
public function __construct(private readonly EquipmentService $service) {}
|
||||
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(
|
||||
fn () => $this->service->index($request->only([
|
||||
'search', 'status', 'production_line', 'equipment_type',
|
||||
'sort_by', 'sort_direction', 'per_page',
|
||||
])),
|
||||
__('message.fetched')
|
||||
);
|
||||
}
|
||||
|
||||
public function show(int $id): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(
|
||||
fn () => $this->service->show($id),
|
||||
__('message.fetched')
|
||||
);
|
||||
}
|
||||
|
||||
public function store(StoreEquipmentRequest $request): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(
|
||||
fn () => $this->service->store($request->validated()),
|
||||
__('message.equipment.created')
|
||||
);
|
||||
}
|
||||
|
||||
public function update(UpdateEquipmentRequest $request, int $id): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(
|
||||
fn () => $this->service->update($id, $request->validated()),
|
||||
__('message.equipment.updated')
|
||||
);
|
||||
}
|
||||
|
||||
public function destroy(int $id): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(
|
||||
fn () => $this->service->destroy($id),
|
||||
__('message.equipment.deleted')
|
||||
);
|
||||
}
|
||||
|
||||
public function restore(int $id): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(
|
||||
fn () => $this->service->restore($id),
|
||||
__('message.equipment.restored')
|
||||
);
|
||||
}
|
||||
|
||||
public function toggleActive(int $id): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(
|
||||
fn () => $this->service->toggleActive($id),
|
||||
__('message.toggled')
|
||||
);
|
||||
}
|
||||
|
||||
public function stats(): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(
|
||||
fn () => $this->service->stats(),
|
||||
__('message.fetched')
|
||||
);
|
||||
}
|
||||
|
||||
public function options(): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(
|
||||
fn () => $this->service->options(),
|
||||
__('message.fetched')
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V1\Equipment;
|
||||
|
||||
use App\Helpers\ApiResponse;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\V1\Equipment\StoreInspectionTemplateRequest;
|
||||
use App\Http\Requests\V1\Equipment\ToggleInspectionDetailRequest;
|
||||
use App\Http\Requests\V1\Equipment\UpdateInspectionNotesRequest;
|
||||
use App\Services\Equipment\EquipmentInspectionService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class EquipmentInspectionController extends Controller
|
||||
{
|
||||
public function __construct(private readonly EquipmentInspectionService $service) {}
|
||||
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(
|
||||
fn () => $this->service->getInspections(
|
||||
$request->input('cycle', 'daily'),
|
||||
$request->input('period', now()->format('Y-m')),
|
||||
$request->input('production_line'),
|
||||
$request->input('equipment_id') ? (int) $request->input('equipment_id') : null
|
||||
),
|
||||
__('message.fetched')
|
||||
);
|
||||
}
|
||||
|
||||
public function toggleDetail(ToggleInspectionDetailRequest $request): JsonResponse
|
||||
{
|
||||
$data = $request->validated();
|
||||
|
||||
return ApiResponse::handle(
|
||||
fn () => $this->service->toggleDetail(
|
||||
$data['equipment_id'],
|
||||
$data['template_item_id'],
|
||||
$data['check_date'],
|
||||
$data['cycle'] ?? 'daily'
|
||||
),
|
||||
__('message.equipment.inspection_saved')
|
||||
);
|
||||
}
|
||||
|
||||
public function setResult(Request $request): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(
|
||||
fn () => $this->service->setResult(
|
||||
(int) $request->input('equipment_id'),
|
||||
(int) $request->input('template_item_id'),
|
||||
$request->input('check_date'),
|
||||
$request->input('cycle', 'daily'),
|
||||
$request->input('result')
|
||||
),
|
||||
__('message.equipment.inspection_saved')
|
||||
);
|
||||
}
|
||||
|
||||
public function updateNotes(UpdateInspectionNotesRequest $request): JsonResponse
|
||||
{
|
||||
$data = $request->validated();
|
||||
|
||||
return ApiResponse::handle(
|
||||
fn () => $this->service->updateNotes(
|
||||
$data['equipment_id'],
|
||||
$data['year_month'],
|
||||
collect($data)->only(['overall_judgment', 'inspector_id', 'repair_note', 'issue_note'])->toArray(),
|
||||
$data['cycle'] ?? 'daily'
|
||||
),
|
||||
__('message.equipment.inspection_saved')
|
||||
);
|
||||
}
|
||||
|
||||
public function resetInspection(Request $request): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(
|
||||
fn () => $this->service->resetInspection(
|
||||
(int) $request->input('equipment_id'),
|
||||
$request->input('cycle', 'daily'),
|
||||
$request->input('period')
|
||||
),
|
||||
__('message.equipment.inspection_reset')
|
||||
);
|
||||
}
|
||||
|
||||
public function templates(int $id): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(
|
||||
fn () => $this->service->getActiveCycles($id),
|
||||
__('message.fetched')
|
||||
);
|
||||
}
|
||||
|
||||
public function storeTemplate(StoreInspectionTemplateRequest $request, int $id): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(
|
||||
fn () => $this->service->saveTemplate($id, $request->validated()),
|
||||
__('message.equipment.template_created')
|
||||
);
|
||||
}
|
||||
|
||||
public function updateTemplate(Request $request, int $templateId): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(
|
||||
fn () => $this->service->updateTemplate($templateId, $request->all()),
|
||||
__('message.updated')
|
||||
);
|
||||
}
|
||||
|
||||
public function deleteTemplate(int $templateId): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(
|
||||
fn () => $this->service->deleteTemplate($templateId),
|
||||
__('message.deleted')
|
||||
);
|
||||
}
|
||||
|
||||
public function copyTemplates(Request $request, int $id): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(
|
||||
fn () => $this->service->copyTemplates(
|
||||
$id,
|
||||
$request->input('source_cycle'),
|
||||
$request->input('target_cycles', [])
|
||||
),
|
||||
__('message.equipment.template_copied')
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V1\Equipment;
|
||||
|
||||
use App\Helpers\ApiResponse;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Equipment\StoreEquipmentPhotoRequest;
|
||||
use App\Services\Equipment\EquipmentPhotoService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
class EquipmentPhotoController extends Controller
|
||||
{
|
||||
public function __construct(private readonly EquipmentPhotoService $service) {}
|
||||
|
||||
public function index(int $id): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(
|
||||
fn () => $this->service->index($id),
|
||||
__('message.fetched')
|
||||
);
|
||||
}
|
||||
|
||||
public function store(StoreEquipmentPhotoRequest $request, int $id): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(
|
||||
fn () => $this->service->store($id, $request->file('files')),
|
||||
__('message.equipment.photo_uploaded')
|
||||
);
|
||||
}
|
||||
|
||||
public function destroy(int $id, int $fileId): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(
|
||||
fn () => $this->service->destroy($id, $fileId),
|
||||
__('message.deleted')
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V1\Equipment;
|
||||
|
||||
use App\Helpers\ApiResponse;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\V1\Equipment\StoreEquipmentRepairRequest;
|
||||
use App\Services\Equipment\EquipmentRepairService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class EquipmentRepairController extends Controller
|
||||
{
|
||||
public function __construct(private readonly EquipmentRepairService $service) {}
|
||||
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(
|
||||
fn () => $this->service->index($request->only([
|
||||
'equipment_id', 'repair_type', 'date_from', 'date_to', 'search', 'per_page',
|
||||
])),
|
||||
__('message.fetched')
|
||||
);
|
||||
}
|
||||
|
||||
public function store(StoreEquipmentRepairRequest $request): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(
|
||||
fn () => $this->service->store($request->validated()),
|
||||
__('message.equipment.repair_created')
|
||||
);
|
||||
}
|
||||
|
||||
public function update(StoreEquipmentRepairRequest $request, int $id): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(
|
||||
fn () => $this->service->update($id, $request->validated()),
|
||||
__('message.updated')
|
||||
);
|
||||
}
|
||||
|
||||
public function destroy(int $id): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(
|
||||
fn () => $this->service->destroy($id),
|
||||
__('message.deleted')
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -117,6 +117,7 @@ public function handle(Request $request, Closure $next)
|
||||
// 화이트리스트(인증 예외 라우트) - Bearer 토큰 없이 접근 가능
|
||||
$allowWithoutAuth = [
|
||||
'api/v1/login',
|
||||
'api/v1/token-login', // MNG → SAM 자동 로그인 (API Key만 필요)
|
||||
'api/v1/signup',
|
||||
'api/v1/register',
|
||||
'api/v1/refresh',
|
||||
|
||||
20
app/Http/Requests/Approval/ApproveRequest.php
Normal file
20
app/Http/Requests/Approval/ApproveRequest.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Approval;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class ApproveRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'comment' => 'nullable|string|max:1000',
|
||||
];
|
||||
}
|
||||
}
|
||||
20
app/Http/Requests/Approval/CancelRequest.php
Normal file
20
app/Http/Requests/Approval/CancelRequest.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Approval;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class CancelRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'recall_reason' => 'nullable|string|max:1000',
|
||||
];
|
||||
}
|
||||
}
|
||||
26
app/Http/Requests/Approval/DelegationStoreRequest.php
Normal file
26
app/Http/Requests/Approval/DelegationStoreRequest.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Approval;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class DelegationStoreRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'delegate_id' => 'required|integer|exists:users,id',
|
||||
'start_date' => 'required|date|after_or_equal:today',
|
||||
'end_date' => 'required|date|after_or_equal:start_date',
|
||||
'form_ids' => 'nullable|array',
|
||||
'form_ids.*' => 'integer|exists:approval_forms,id',
|
||||
'notify_delegator' => 'nullable|boolean',
|
||||
'reason' => 'nullable|string|max:500',
|
||||
];
|
||||
}
|
||||
}
|
||||
27
app/Http/Requests/Approval/DelegationUpdateRequest.php
Normal file
27
app/Http/Requests/Approval/DelegationUpdateRequest.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Approval;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class DelegationUpdateRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'delegate_id' => 'nullable|integer|exists:users,id',
|
||||
'start_date' => 'nullable|date',
|
||||
'end_date' => 'nullable|date|after_or_equal:start_date',
|
||||
'form_ids' => 'nullable|array',
|
||||
'form_ids.*' => 'integer|exists:approval_forms,id',
|
||||
'notify_delegator' => 'nullable|boolean',
|
||||
'is_active' => 'nullable|boolean',
|
||||
'reason' => 'nullable|string|max:500',
|
||||
];
|
||||
}
|
||||
}
|
||||
27
app/Http/Requests/Approval/HoldRequest.php
Normal file
27
app/Http/Requests/Approval/HoldRequest.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Approval;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class HoldRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'comment' => 'required|string|max:1000',
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'comment.required' => __('error.approval.comment_required'),
|
||||
];
|
||||
}
|
||||
}
|
||||
20
app/Http/Requests/Approval/PreDecideRequest.php
Normal file
20
app/Http/Requests/Approval/PreDecideRequest.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Approval;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class PreDecideRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'comment' => 'nullable|string|max:1000',
|
||||
];
|
||||
}
|
||||
}
|
||||
53
app/Http/Requests/Equipment/StoreEquipmentPhotoRequest.php
Normal file
53
app/Http/Requests/Equipment/StoreEquipmentPhotoRequest.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Equipment;
|
||||
|
||||
use App\Models\Commons\File;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class StoreEquipmentPhotoRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
$equipmentId = $this->route('id');
|
||||
$currentCount = File::where('document_id', $equipmentId)
|
||||
->where('document_type', 'equipment')
|
||||
->whereNull('deleted_at')
|
||||
->count();
|
||||
|
||||
$maxFiles = 10 - $currentCount;
|
||||
|
||||
return [
|
||||
'files' => ['required', 'array', 'min:1', "max:{$maxFiles}"],
|
||||
'files.*' => [
|
||||
'required',
|
||||
'file',
|
||||
'mimes:jpg,jpeg,png,gif,bmp,webp',
|
||||
'max:10240', // 10MB
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function attributes(): array
|
||||
{
|
||||
return [
|
||||
'files' => '사진 파일',
|
||||
'files.*' => '사진 파일',
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'files.required' => __('error.file.required'),
|
||||
'files.max' => __('error.equipment.photo_limit_exceeded'),
|
||||
'files.*.mimes' => __('error.file.invalid_type'),
|
||||
'files.*.max' => __('error.file.size_exceeded'),
|
||||
];
|
||||
}
|
||||
}
|
||||
39
app/Http/Requests/Qms/AuditChecklistStoreRequest.php
Normal file
39
app/Http/Requests/Qms/AuditChecklistStoreRequest.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Qms;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class AuditChecklistStoreRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'year' => 'required|integer|min:2020|max:2100',
|
||||
'quarter' => 'required|integer|in:1,2,3,4',
|
||||
'type' => 'nullable|string|max:30',
|
||||
'categories' => 'required|array|min:1',
|
||||
'categories.*.title' => 'required|string|max:200',
|
||||
'categories.*.sort_order' => 'nullable|integer|min:0',
|
||||
'categories.*.items' => 'required|array|min:1',
|
||||
'categories.*.items.*.name' => 'required|string|max:200',
|
||||
'categories.*.items.*.description' => 'nullable|string',
|
||||
'categories.*.items.*.sort_order' => 'nullable|integer|min:0',
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'categories.required' => __('validation.required', ['attribute' => '카테고리']),
|
||||
'categories.*.title.required' => __('validation.required', ['attribute' => '카테고리 제목']),
|
||||
'categories.*.items.required' => __('validation.required', ['attribute' => '점검 항목']),
|
||||
'categories.*.items.*.name.required' => __('validation.required', ['attribute' => '항목명']),
|
||||
];
|
||||
}
|
||||
}
|
||||
28
app/Http/Requests/Qms/AuditChecklistUpdateRequest.php
Normal file
28
app/Http/Requests/Qms/AuditChecklistUpdateRequest.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Qms;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class AuditChecklistUpdateRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'categories' => 'sometimes|array|min:1',
|
||||
'categories.*.id' => 'nullable|integer|exists:audit_checklist_categories,id',
|
||||
'categories.*.title' => 'required|string|max:200',
|
||||
'categories.*.sort_order' => 'nullable|integer|min:0',
|
||||
'categories.*.items' => 'required|array|min:1',
|
||||
'categories.*.items.*.id' => 'nullable|integer|exists:audit_checklist_items,id',
|
||||
'categories.*.items.*.name' => 'required|string|max:200',
|
||||
'categories.*.items.*.description' => 'nullable|string',
|
||||
'categories.*.items.*.sort_order' => 'nullable|integer|min:0',
|
||||
];
|
||||
}
|
||||
}
|
||||
28
app/Http/Requests/Qms/QmsLotAuditConfirmRequest.php
Normal file
28
app/Http/Requests/Qms/QmsLotAuditConfirmRequest.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Qms;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class QmsLotAuditConfirmRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'confirmed' => 'required|boolean',
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'confirmed.required' => __('validation.required', ['attribute' => '확인 상태']),
|
||||
'confirmed.boolean' => __('validation.boolean', ['attribute' => '확인 상태']),
|
||||
];
|
||||
}
|
||||
}
|
||||
34
app/Http/Requests/Qms/QmsLotAuditDocumentDetailRequest.php
Normal file
34
app/Http/Requests/Qms/QmsLotAuditDocumentDetailRequest.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Qms;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class QmsLotAuditDocumentDetailRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function prepareForValidation(): void
|
||||
{
|
||||
$this->merge([
|
||||
'type' => $this->route('type'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'type' => 'required|string|in:import,order,log,report,confirmation,shipping,product,quality',
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'type.in' => __('validation.in', ['attribute' => '서류 타입']),
|
||||
];
|
||||
}
|
||||
}
|
||||
23
app/Http/Requests/Qms/QmsLotAuditIndexRequest.php
Normal file
23
app/Http/Requests/Qms/QmsLotAuditIndexRequest.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Qms;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class QmsLotAuditIndexRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'year' => 'nullable|integer|min:2020|max:2100',
|
||||
'quarter' => 'nullable|integer|in:1,2,3,4',
|
||||
'q' => 'nullable|string|max:100',
|
||||
'per_page' => 'nullable|integer|min:1|max:100',
|
||||
];
|
||||
}
|
||||
}
|
||||
40
app/Http/Requests/Quality/SaveChecklistTemplateRequest.php
Normal file
40
app/Http/Requests/Quality/SaveChecklistTemplateRequest.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Quality;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class SaveChecklistTemplateRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => ['nullable', 'string', 'max:255'],
|
||||
'categories' => ['required', 'array', 'min:1'],
|
||||
'categories.*.id' => ['required', 'string', 'max:50'],
|
||||
'categories.*.title' => ['required', 'string', 'max:255'],
|
||||
'categories.*.subItems' => ['required', 'array'],
|
||||
'categories.*.subItems.*.id' => ['required', 'string', 'max:50'],
|
||||
'categories.*.subItems.*.name' => ['required', 'string', 'max:255'],
|
||||
'options' => ['nullable', 'array'],
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'categories.required' => __('validation.required', ['attribute' => '카테고리']),
|
||||
'categories.min' => __('validation.min.array', ['attribute' => '카테고리', 'min' => 1]),
|
||||
'categories.*.id.required' => __('validation.required', ['attribute' => '카테고리 ID']),
|
||||
'categories.*.title.required' => __('validation.required', ['attribute' => '카테고리 제목']),
|
||||
'categories.*.subItems.required' => __('validation.required', ['attribute' => '점검항목']),
|
||||
'categories.*.subItems.*.id.required' => __('validation.required', ['attribute' => '항목 ID']),
|
||||
'categories.*.subItems.*.name.required' => __('validation.required', ['attribute' => '항목명']),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -34,7 +34,7 @@ public function rules(): array
|
||||
'items.*.productCategory' => 'nullable|string|in:SCREEN,STEEL',
|
||||
'items.*.guideRailType' => 'nullable|string|in:wall,ceiling,floor,mixed',
|
||||
'items.*.motorPower' => 'nullable|string|in:single,three',
|
||||
'items.*.controller' => 'nullable|string|in:basic,smart,premium',
|
||||
'items.*.controller' => 'nullable|string|in:exposed,embedded,embedded_no_box',
|
||||
'items.*.wingSize' => 'nullable|numeric|min:0|max:500',
|
||||
'items.*.inspectionFee' => 'nullable|numeric|min:0',
|
||||
|
||||
@@ -45,7 +45,7 @@ public function rules(): array
|
||||
'items.*.PC' => 'nullable|string|in:SCREEN,STEEL',
|
||||
'items.*.GT' => 'nullable|string|in:wall,ceiling,floor,mixed',
|
||||
'items.*.MP' => 'nullable|string|in:single,three',
|
||||
'items.*.CT' => 'nullable|string|in:basic,smart,premium',
|
||||
'items.*.CT' => 'nullable|string|in:exposed,embedded,embedded_no_box',
|
||||
'items.*.WS' => 'nullable|numeric|min:0|max:500',
|
||||
'items.*.INSP' => 'nullable|numeric|min:0',
|
||||
|
||||
@@ -128,7 +128,7 @@ private function normalizeInputVariables(array $item): array
|
||||
'PC' => $item['productCategory'] ?? $item['PC'] ?? 'SCREEN',
|
||||
'GT' => $item['guideRailType'] ?? $item['GT'] ?? 'wall',
|
||||
'MP' => $item['motorPower'] ?? $item['MP'] ?? 'single',
|
||||
'CT' => $item['controller'] ?? $item['CT'] ?? 'basic',
|
||||
'CT' => $item['controller'] ?? $item['CT'] ?? 'exposed',
|
||||
'WS' => (float) ($item['wingSize'] ?? $item['WS'] ?? 50),
|
||||
'INSP' => (float) ($item['inspectionFee'] ?? $item['INSP'] ?? 50000),
|
||||
];
|
||||
|
||||
@@ -30,7 +30,7 @@ public function rules(): array
|
||||
'PC' => 'nullable|string|in:SCREEN,STEEL',
|
||||
'GT' => 'nullable|string|in:wall,ceiling,floor,mixed',
|
||||
'MP' => 'nullable|string|in:single,three',
|
||||
'CT' => 'nullable|string|in:basic,smart,premium',
|
||||
'CT' => 'nullable|string|in:exposed,embedded,embedded_no_box',
|
||||
'WS' => 'nullable|numeric|min:0|max:500',
|
||||
'INSP' => 'nullable|numeric|min:0',
|
||||
|
||||
@@ -82,7 +82,7 @@ public function getInputVariables(): array
|
||||
'PC' => $validated['PC'] ?? 'SCREEN',
|
||||
'GT' => $validated['GT'] ?? 'wall',
|
||||
'MP' => $validated['MP'] ?? 'single',
|
||||
'CT' => $validated['CT'] ?? 'basic',
|
||||
'CT' => $validated['CT'] ?? 'exposed',
|
||||
'WS' => (float) ($validated['WS'] ?? 50),
|
||||
'INSP' => (float) ($validated['INSP'] ?? 50000),
|
||||
];
|
||||
|
||||
@@ -20,18 +20,18 @@ public function rules(): array
|
||||
'issue_type' => ['required', 'string', Rule::in(TaxInvoice::ISSUE_TYPES)],
|
||||
'direction' => ['required', 'string', Rule::in(TaxInvoice::DIRECTIONS)],
|
||||
|
||||
// 공급자 정보
|
||||
'supplier_corp_num' => ['required', 'string', 'max:20'],
|
||||
'supplier_corp_name' => ['required', 'string', 'max:100'],
|
||||
// 공급자 정보 (매입 시 필수, 매출 시 선택)
|
||||
'supplier_corp_num' => ['required_if:direction,purchases', 'nullable', 'string', 'max:20'],
|
||||
'supplier_corp_name' => ['required_if:direction,purchases', 'nullable', 'string', 'max:100'],
|
||||
'supplier_ceo_name' => ['nullable', 'string', 'max:50'],
|
||||
'supplier_addr' => ['nullable', 'string', 'max:200'],
|
||||
'supplier_biz_type' => ['nullable', 'string', 'max:100'],
|
||||
'supplier_biz_class' => ['nullable', 'string', 'max:100'],
|
||||
'supplier_contact_id' => ['nullable', 'string', 'email', 'max:100'],
|
||||
|
||||
// 공급받는자 정보
|
||||
'buyer_corp_num' => ['required', 'string', 'max:20'],
|
||||
'buyer_corp_name' => ['required', 'string', 'max:100'],
|
||||
// 공급받는자 정보 (매출 시 필수, 매입 시 선택)
|
||||
'buyer_corp_num' => ['required_if:direction,sales', 'nullable', 'string', 'max:20'],
|
||||
'buyer_corp_name' => ['required_if:direction,sales', 'nullable', 'string', 'max:100'],
|
||||
'buyer_ceo_name' => ['nullable', 'string', 'max:50'],
|
||||
'buyer_addr' => ['nullable', 'string', 'max:200'],
|
||||
'buyer_biz_type' => ['nullable', 'string', 'max:100'],
|
||||
|
||||
@@ -17,6 +17,12 @@ public function rules(): array
|
||||
'code' => ['required', 'string', 'max:10'],
|
||||
'name' => ['required', 'string', 'max:100'],
|
||||
'category' => ['nullable', 'string', 'in:asset,liability,capital,revenue,expense'],
|
||||
'sub_category' => ['nullable', 'string', 'max:50'],
|
||||
'parent_code' => ['nullable', 'string', 'max:10'],
|
||||
'depth' => ['nullable', 'integer', 'in:1,2,3'],
|
||||
'department_type' => ['nullable', 'string', 'in:common,manufacturing,admin'],
|
||||
'description' => ['nullable', 'string', 'max:500'],
|
||||
'sort_order' => ['nullable', 'integer'],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -26,6 +32,8 @@ public function messages(): array
|
||||
'code.required' => '계정과목 코드를 입력하세요.',
|
||||
'name.required' => '계정과목명을 입력하세요.',
|
||||
'category.in' => '유효한 분류를 선택하세요.',
|
||||
'depth.in' => '계층은 1(대), 2(중), 3(소) 중 하나여야 합니다.',
|
||||
'department_type.in' => '부문은 common, manufacturing, admin 중 하나여야 합니다.',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\V1\AccountSubject;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class UpdateAccountSubjectRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => ['sometimes', 'string', 'max:100'],
|
||||
'category' => ['nullable', 'string', 'in:asset,liability,capital,revenue,expense'],
|
||||
'sub_category' => ['nullable', 'string', 'max:50'],
|
||||
'parent_code' => ['nullable', 'string', 'max:10'],
|
||||
'depth' => ['nullable', 'integer', 'in:1,2,3'],
|
||||
'department_type' => ['nullable', 'string', 'in:common,manufacturing,admin'],
|
||||
'description' => ['nullable', 'string', 'max:500'],
|
||||
'sort_order' => ['nullable', 'integer'],
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'category.in' => '유효한 분류를 선택하세요.',
|
||||
'depth.in' => '계층은 1(대), 2(중), 3(소) 중 하나여야 합니다.',
|
||||
'department_type.in' => '부문은 common, manufacturing, admin 중 하나여야 합니다.',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\V1\Equipment;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class StoreEquipmentRepairRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'equipment_id' => 'required|integer|exists:equipments,id',
|
||||
'repair_date' => 'required|date',
|
||||
'repair_type' => 'nullable|string|in:internal,external',
|
||||
'repair_hours' => 'nullable|numeric|min:0',
|
||||
'description' => 'nullable|string',
|
||||
'cost' => 'nullable|numeric|min:0',
|
||||
'vendor' => 'nullable|string|max:100',
|
||||
'repaired_by' => 'nullable|integer|exists:users,id',
|
||||
'memo' => 'nullable|string',
|
||||
'options' => 'nullable|array',
|
||||
];
|
||||
}
|
||||
}
|
||||
41
app/Http/Requests/V1/Equipment/StoreEquipmentRequest.php
Normal file
41
app/Http/Requests/V1/Equipment/StoreEquipmentRequest.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\V1\Equipment;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class StoreEquipmentRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'equipment_code' => 'required|string|max:50',
|
||||
'name' => 'required|string|max:100',
|
||||
'equipment_type' => 'nullable|string|max:50',
|
||||
'specification' => 'nullable|string|max:200',
|
||||
'manufacturer' => 'nullable|string|max:100',
|
||||
'model_name' => 'nullable|string|max:100',
|
||||
'serial_no' => 'nullable|string|max:100',
|
||||
'location' => 'nullable|string|max:100',
|
||||
'production_line' => 'nullable|string|max:50',
|
||||
'purchase_date' => 'nullable|date',
|
||||
'install_date' => 'nullable|date',
|
||||
'purchase_price' => 'nullable|numeric|min:0',
|
||||
'useful_life' => 'nullable|integer|min:0',
|
||||
'status' => 'nullable|in:active,idle,disposed',
|
||||
'disposed_date' => 'nullable|date',
|
||||
'manager_id' => 'nullable|integer|exists:users,id',
|
||||
'sub_manager_id' => 'nullable|integer|exists:users,id',
|
||||
'photo_path' => 'nullable|string|max:500',
|
||||
'memo' => 'nullable|string',
|
||||
'options' => 'nullable|array',
|
||||
'is_active' => 'nullable|boolean',
|
||||
'sort_order' => 'nullable|integer|min:0',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\V1\Equipment;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class StoreInspectionTemplateRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'inspection_cycle' => 'required|string|in:daily,weekly,monthly,bimonthly,quarterly,semiannual',
|
||||
'item_no' => 'required|string|max:20',
|
||||
'check_point' => 'required|string|max:100',
|
||||
'check_item' => 'required|string|max:200',
|
||||
'check_timing' => 'nullable|string|max:50',
|
||||
'check_frequency' => 'nullable|string|max:50',
|
||||
'check_method' => 'nullable|string|max:200',
|
||||
'sort_order' => 'nullable|integer|min:0',
|
||||
'is_active' => 'nullable|boolean',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\V1\Equipment;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class ToggleInspectionDetailRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'equipment_id' => 'required|integer|exists:equipments,id',
|
||||
'template_item_id' => 'required|integer|exists:equipment_inspection_templates,id',
|
||||
'check_date' => 'required|date',
|
||||
'cycle' => 'nullable|string|in:daily,weekly,monthly,bimonthly,quarterly,semiannual',
|
||||
];
|
||||
}
|
||||
}
|
||||
41
app/Http/Requests/V1/Equipment/UpdateEquipmentRequest.php
Normal file
41
app/Http/Requests/V1/Equipment/UpdateEquipmentRequest.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\V1\Equipment;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class UpdateEquipmentRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'equipment_code' => 'sometimes|string|max:50',
|
||||
'name' => 'sometimes|string|max:100',
|
||||
'equipment_type' => 'nullable|string|max:50',
|
||||
'specification' => 'nullable|string|max:200',
|
||||
'manufacturer' => 'nullable|string|max:100',
|
||||
'model_name' => 'nullable|string|max:100',
|
||||
'serial_no' => 'nullable|string|max:100',
|
||||
'location' => 'nullable|string|max:100',
|
||||
'production_line' => 'nullable|string|max:50',
|
||||
'purchase_date' => 'nullable|date',
|
||||
'install_date' => 'nullable|date',
|
||||
'purchase_price' => 'nullable|numeric|min:0',
|
||||
'useful_life' => 'nullable|integer|min:0',
|
||||
'status' => 'nullable|in:active,idle,disposed',
|
||||
'disposed_date' => 'nullable|date',
|
||||
'manager_id' => 'nullable|integer|exists:users,id',
|
||||
'sub_manager_id' => 'nullable|integer|exists:users,id',
|
||||
'photo_path' => 'nullable|string|max:500',
|
||||
'memo' => 'nullable|string',
|
||||
'options' => 'nullable|array',
|
||||
'is_active' => 'nullable|boolean',
|
||||
'sort_order' => 'nullable|integer|min:0',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\V1\Equipment;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class UpdateInspectionNotesRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'equipment_id' => 'required|integer|exists:equipments,id',
|
||||
'year_month' => 'required|string',
|
||||
'cycle' => 'nullable|string|in:daily,weekly,monthly,bimonthly,quarterly,semiannual',
|
||||
'overall_judgment' => 'nullable|string|in:OK,NG',
|
||||
'inspector_id' => 'nullable|integer|exists:users,id',
|
||||
'repair_note' => 'nullable|string',
|
||||
'issue_note' => 'nullable|string',
|
||||
];
|
||||
}
|
||||
}
|
||||
29
app/Http/Requests/V1/Payroll/BulkGeneratePayrollRequest.php
Normal file
29
app/Http/Requests/V1/Payroll/BulkGeneratePayrollRequest.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\V1\Payroll;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class BulkGeneratePayrollRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'year' => ['required', 'integer', 'min:2000', 'max:2100'],
|
||||
'month' => ['required', 'integer', 'min:1', 'max:12'],
|
||||
];
|
||||
}
|
||||
|
||||
public function attributes(): array
|
||||
{
|
||||
return [
|
||||
'year' => __('validation.attributes.pay_year'),
|
||||
'month' => __('validation.attributes.pay_month'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\V1\Payroll;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class CopyFromPreviousPayrollRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'year' => ['required', 'integer', 'min:2000', 'max:2100'],
|
||||
'month' => ['required', 'integer', 'min:1', 'max:12'],
|
||||
];
|
||||
}
|
||||
|
||||
public function attributes(): array
|
||||
{
|
||||
return [
|
||||
'year' => __('validation.attributes.pay_year'),
|
||||
'month' => __('validation.attributes.pay_month'),
|
||||
];
|
||||
}
|
||||
}
|
||||
31
app/Http/Requests/V1/Payroll/StorePayrollJournalRequest.php
Normal file
31
app/Http/Requests/V1/Payroll/StorePayrollJournalRequest.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\V1\Payroll;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class StorePayrollJournalRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'year' => ['required', 'integer', 'min:2000', 'max:2100'],
|
||||
'month' => ['required', 'integer', 'min:1', 'max:12'],
|
||||
'entry_date' => ['nullable', 'date_format:Y-m-d'],
|
||||
];
|
||||
}
|
||||
|
||||
public function attributes(): array
|
||||
{
|
||||
return [
|
||||
'year' => __('validation.attributes.pay_year'),
|
||||
'month' => __('validation.attributes.pay_month'),
|
||||
'entry_date' => __('validation.attributes.entry_date'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,14 @@ public function rules(): array
|
||||
'deductions' => ['nullable', 'array'],
|
||||
'deductions.*.name' => ['required_with:deductions', 'string', 'max:50'],
|
||||
'deductions.*.amount' => ['required_with:deductions', 'numeric', 'min:0'],
|
||||
'deduction_overrides' => ['nullable', 'array'],
|
||||
'deduction_overrides.income_tax' => ['nullable', 'numeric', 'min:0'],
|
||||
'deduction_overrides.resident_tax' => ['nullable', 'numeric', 'min:0'],
|
||||
'deduction_overrides.health_insurance' => ['nullable', 'numeric', 'min:0'],
|
||||
'deduction_overrides.long_term_care' => ['nullable', 'numeric', 'min:0'],
|
||||
'deduction_overrides.pension' => ['nullable', 'numeric', 'min:0'],
|
||||
'deduction_overrides.employment_insurance' => ['nullable', 'numeric', 'min:0'],
|
||||
'family_count' => ['nullable', 'integer', 'min:1', 'max:11'],
|
||||
'note' => ['nullable', 'string', 'max:1000'],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -31,6 +31,14 @@ public function rules(): array
|
||||
'deductions' => ['nullable', 'array'],
|
||||
'deductions.*.name' => ['required_with:deductions', 'string', 'max:50'],
|
||||
'deductions.*.amount' => ['required_with:deductions', 'numeric', 'min:0'],
|
||||
'deduction_overrides' => ['nullable', 'array'],
|
||||
'deduction_overrides.income_tax' => ['nullable', 'numeric', 'min:0'],
|
||||
'deduction_overrides.resident_tax' => ['nullable', 'numeric', 'min:0'],
|
||||
'deduction_overrides.health_insurance' => ['nullable', 'numeric', 'min:0'],
|
||||
'deduction_overrides.long_term_care' => ['nullable', 'numeric', 'min:0'],
|
||||
'deduction_overrides.pension' => ['nullable', 'numeric', 'min:0'],
|
||||
'deduction_overrides.employment_insurance' => ['nullable', 'numeric', 'min:0'],
|
||||
'_is_super_admin' => ['nullable', 'boolean'],
|
||||
'note' => ['nullable', 'string', 'max:1000'],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ public function rules(): array
|
||||
'remark' => ['nullable', 'string', 'max:1000'],
|
||||
'manufacturer' => ['nullable', 'string', 'max:100'],
|
||||
'material_no' => ['nullable', 'string', 'max:50'],
|
||||
'certificate_file_id' => ['nullable', 'integer', 'exists:files,id'],
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ public function rules(): array
|
||||
'inspection_result' => ['nullable', 'string', 'max:20'],
|
||||
'manufacturer' => ['nullable', 'string', 'max:100'],
|
||||
'material_no' => ['nullable', 'string', 'max:50'],
|
||||
'certificate_file_id' => ['nullable', 'integer', 'exists:files,id'],
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
34
app/Models/Barobill/BarobillBankSyncStatus.php
Normal file
34
app/Models/Barobill/BarobillBankSyncStatus.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Barobill;
|
||||
|
||||
use App\Traits\BelongsToTenant;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class BarobillBankSyncStatus extends Model
|
||||
{
|
||||
use BelongsToTenant;
|
||||
|
||||
protected $table = 'barobill_bank_sync_status';
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'bank_account_num',
|
||||
'synced_year_month',
|
||||
'synced_at',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'synced_at' => 'datetime',
|
||||
];
|
||||
|
||||
// =========================================================================
|
||||
// 관계 정의
|
||||
// =========================================================================
|
||||
|
||||
public function tenant(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(\App\Models\Tenants\Tenant::class);
|
||||
}
|
||||
}
|
||||
97
app/Models/Barobill/BarobillBankTransaction.php
Normal file
97
app/Models/Barobill/BarobillBankTransaction.php
Normal file
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Barobill;
|
||||
|
||||
use App\Traits\BelongsToTenant;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class BarobillBankTransaction extends Model
|
||||
{
|
||||
use BelongsToTenant;
|
||||
|
||||
protected $table = 'barobill_bank_transactions';
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'bank_account_num',
|
||||
'bank_code',
|
||||
'bank_name',
|
||||
'trans_date',
|
||||
'trans_time',
|
||||
'trans_dt',
|
||||
'deposit',
|
||||
'withdraw',
|
||||
'balance',
|
||||
'summary',
|
||||
'cast',
|
||||
'memo',
|
||||
'trans_office',
|
||||
'account_code',
|
||||
'account_name',
|
||||
'is_manual',
|
||||
'client_code',
|
||||
'client_name',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'deposit' => 'decimal:2',
|
||||
'withdraw' => 'decimal:2',
|
||||
'balance' => 'decimal:2',
|
||||
'is_manual' => 'boolean',
|
||||
];
|
||||
|
||||
// =========================================================================
|
||||
// 관계 정의
|
||||
// =========================================================================
|
||||
|
||||
public function tenant(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(\App\Models\Tenants\Tenant::class);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 접근자
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 거래 고유 키 (계좌번호|거래일시|입금|출금|잔액)
|
||||
*/
|
||||
public function getUniqueKeyAttribute(): string
|
||||
{
|
||||
return static::generateUniqueKey([
|
||||
'bank_account_num' => $this->bank_account_num,
|
||||
'trans_dt' => $this->trans_dt,
|
||||
'deposit' => $this->deposit,
|
||||
'withdraw' => $this->withdraw,
|
||||
'balance' => $this->balance,
|
||||
]);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 헬퍼 메서드
|
||||
// =========================================================================
|
||||
|
||||
public static function generateUniqueKey(array $data): string
|
||||
{
|
||||
return implode('|', [
|
||||
$data['bank_account_num'] ?? '',
|
||||
$data['trans_dt'] ?? '',
|
||||
$data['deposit'] ?? '0',
|
||||
$data['withdraw'] ?? '0',
|
||||
$data['balance'] ?? '0',
|
||||
]);
|
||||
}
|
||||
|
||||
public static function getByDateRange(int $tenantId, string $startDate, string $endDate, ?string $accountNum = null)
|
||||
{
|
||||
$query = static::where('tenant_id', $tenantId)
|
||||
->whereBetween('trans_date', [$startDate, $endDate]);
|
||||
|
||||
if ($accountNum) {
|
||||
$query->where('bank_account_num', $accountNum);
|
||||
}
|
||||
|
||||
return $query->orderBy('trans_date')->orderBy('trans_dt')->get();
|
||||
}
|
||||
}
|
||||
49
app/Models/Barobill/BarobillBankTransactionOverride.php
Normal file
49
app/Models/Barobill/BarobillBankTransactionOverride.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Barobill;
|
||||
|
||||
use App\Traits\BelongsToTenant;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class BarobillBankTransactionOverride extends Model
|
||||
{
|
||||
use BelongsToTenant;
|
||||
|
||||
protected $table = 'barobill_bank_transaction_overrides';
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'unique_key',
|
||||
'modified_summary',
|
||||
'modified_cast',
|
||||
];
|
||||
|
||||
// =========================================================================
|
||||
// 스코프
|
||||
// =========================================================================
|
||||
|
||||
public function scopeByUniqueKey($query, string $uniqueKey)
|
||||
{
|
||||
return $query->where('unique_key', $uniqueKey);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 헬퍼 메서드
|
||||
// =========================================================================
|
||||
|
||||
public static function getByUniqueKeys(int $tenantId, array $uniqueKeys)
|
||||
{
|
||||
return static::where('tenant_id', $tenantId)
|
||||
->whereIn('unique_key', $uniqueKeys)
|
||||
->get()
|
||||
->keyBy('unique_key');
|
||||
}
|
||||
|
||||
public static function saveOverride(int $tenantId, string $uniqueKey, ?string $modifiedSummary, ?string $modifiedCast): self
|
||||
{
|
||||
return static::updateOrCreate(
|
||||
['tenant_id' => $tenantId, 'unique_key' => $uniqueKey],
|
||||
['modified_summary' => $modifiedSummary, 'modified_cast' => $modifiedCast]
|
||||
);
|
||||
}
|
||||
}
|
||||
69
app/Models/Barobill/BarobillBankTransactionSplit.php
Normal file
69
app/Models/Barobill/BarobillBankTransactionSplit.php
Normal file
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Barobill;
|
||||
|
||||
use App\Traits\BelongsToTenant;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class BarobillBankTransactionSplit extends Model
|
||||
{
|
||||
use BelongsToTenant;
|
||||
|
||||
protected $table = 'barobill_bank_transaction_splits';
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'original_unique_key',
|
||||
'split_amount',
|
||||
'account_code',
|
||||
'account_name',
|
||||
'description',
|
||||
'memo',
|
||||
'sort_order',
|
||||
'bank_account_num',
|
||||
'trans_dt',
|
||||
'trans_date',
|
||||
'original_deposit',
|
||||
'original_withdraw',
|
||||
'summary',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'split_amount' => 'decimal:2',
|
||||
'original_deposit' => 'decimal:2',
|
||||
'original_withdraw' => 'decimal:2',
|
||||
'sort_order' => 'integer',
|
||||
];
|
||||
|
||||
// =========================================================================
|
||||
// 관계 정의
|
||||
// =========================================================================
|
||||
|
||||
public function tenant(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(\App\Models\Tenants\Tenant::class);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 헬퍼 메서드
|
||||
// =========================================================================
|
||||
|
||||
public static function getByDateRange(int $tenantId, string $startDate, string $endDate)
|
||||
{
|
||||
return static::where('tenant_id', $tenantId)
|
||||
->whereBetween('trans_date', [$startDate, $endDate])
|
||||
->orderBy('original_unique_key')
|
||||
->orderBy('sort_order')
|
||||
->get()
|
||||
->groupBy('original_unique_key');
|
||||
}
|
||||
|
||||
public static function getByUniqueKey(int $tenantId, string $uniqueKey)
|
||||
{
|
||||
return static::where('tenant_id', $tenantId)
|
||||
->where('original_unique_key', $uniqueKey)
|
||||
->orderBy('sort_order')
|
||||
->get();
|
||||
}
|
||||
}
|
||||
82
app/Models/Barobill/BarobillBillingRecord.php
Normal file
82
app/Models/Barobill/BarobillBillingRecord.php
Normal file
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Barobill;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class BarobillBillingRecord extends Model
|
||||
{
|
||||
protected $table = 'barobill_billing_records';
|
||||
|
||||
public const SERVICE_TYPES = ['tax_invoice', 'bank_account', 'card', 'hometax'];
|
||||
|
||||
public const BILLING_TYPES = ['subscription', 'usage'];
|
||||
|
||||
protected $fillable = [
|
||||
'member_id',
|
||||
'billing_month',
|
||||
'service_type',
|
||||
'billing_type',
|
||||
'quantity',
|
||||
'unit_price',
|
||||
'total_amount',
|
||||
'billed_at',
|
||||
'description',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'quantity' => 'integer',
|
||||
'unit_price' => 'integer',
|
||||
'total_amount' => 'integer',
|
||||
'billed_at' => 'date',
|
||||
];
|
||||
|
||||
// =========================================================================
|
||||
// 관계 정의
|
||||
// =========================================================================
|
||||
|
||||
public function member(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(BarobillMember::class, 'member_id');
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 스코프
|
||||
// =========================================================================
|
||||
|
||||
public function scopeOfMonth($query, string $billingMonth)
|
||||
{
|
||||
return $query->where('billing_month', $billingMonth);
|
||||
}
|
||||
|
||||
public function scopeSubscription($query)
|
||||
{
|
||||
return $query->where('billing_type', 'subscription');
|
||||
}
|
||||
|
||||
public function scopeUsage($query)
|
||||
{
|
||||
return $query->where('billing_type', 'usage');
|
||||
}
|
||||
|
||||
public function scopeOfService($query, string $serviceType)
|
||||
{
|
||||
return $query->where('service_type', $serviceType);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 접근자
|
||||
// =========================================================================
|
||||
|
||||
public function getServiceTypeLabelAttribute(): string
|
||||
{
|
||||
return match ($this->service_type) {
|
||||
'tax_invoice' => '전자세금계산서',
|
||||
'bank_account' => '계좌조회',
|
||||
'card' => '카드조회',
|
||||
'hometax' => '홈택스',
|
||||
default => $this->service_type,
|
||||
};
|
||||
}
|
||||
}
|
||||
108
app/Models/Barobill/BarobillCardTransaction.php
Normal file
108
app/Models/Barobill/BarobillCardTransaction.php
Normal file
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Barobill;
|
||||
|
||||
use App\Traits\BelongsToTenant;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class BarobillCardTransaction extends Model
|
||||
{
|
||||
use BelongsToTenant;
|
||||
|
||||
protected $table = 'barobill_card_transactions';
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'card_num',
|
||||
'card_company',
|
||||
'card_company_name',
|
||||
'use_dt',
|
||||
'use_date',
|
||||
'use_time',
|
||||
'approval_num',
|
||||
'approval_type',
|
||||
'approval_amount',
|
||||
'tax',
|
||||
'service_charge',
|
||||
'payment_plan',
|
||||
'currency_code',
|
||||
'merchant_name',
|
||||
'merchant_biz_num',
|
||||
'merchant_addr',
|
||||
'merchant_ceo',
|
||||
'merchant_biz_type',
|
||||
'merchant_tel',
|
||||
'memo',
|
||||
'use_key',
|
||||
'account_code',
|
||||
'account_name',
|
||||
'deduction_type',
|
||||
'evidence_name',
|
||||
'description',
|
||||
'modified_supply_amount',
|
||||
'modified_tax',
|
||||
'is_manual',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'approval_amount' => 'decimal:2',
|
||||
'tax' => 'decimal:2',
|
||||
'service_charge' => 'decimal:2',
|
||||
'modified_supply_amount' => 'decimal:2',
|
||||
'modified_tax' => 'decimal:2',
|
||||
'is_manual' => 'boolean',
|
||||
];
|
||||
|
||||
// =========================================================================
|
||||
// 관계 정의
|
||||
// =========================================================================
|
||||
|
||||
public function tenant(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(\App\Models\Tenants\Tenant::class);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 접근자
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 거래 고유 키 (cardNum|useDt|approvalNum|approvalAmount)
|
||||
*/
|
||||
public function getUniqueKeyAttribute(): string
|
||||
{
|
||||
return static::generateUniqueKey([
|
||||
'card_num' => $this->card_num,
|
||||
'use_dt' => $this->use_dt,
|
||||
'approval_num' => $this->approval_num,
|
||||
'approval_amount' => $this->approval_amount,
|
||||
]);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 헬퍼 메서드
|
||||
// =========================================================================
|
||||
|
||||
public static function generateUniqueKey(array $data): string
|
||||
{
|
||||
return implode('|', [
|
||||
$data['card_num'] ?? '',
|
||||
$data['use_dt'] ?? '',
|
||||
$data['approval_num'] ?? '',
|
||||
$data['approval_amount'] ?? '0',
|
||||
]);
|
||||
}
|
||||
|
||||
public static function getByDateRange(int $tenantId, string $startDate, string $endDate, ?string $cardNum = null)
|
||||
{
|
||||
$query = static::where('tenant_id', $tenantId)
|
||||
->whereBetween('use_date', [$startDate, $endDate]);
|
||||
|
||||
if ($cardNum) {
|
||||
$query->where('card_num', $cardNum);
|
||||
}
|
||||
|
||||
return $query->orderBy('use_date')->orderBy('use_dt')->get();
|
||||
}
|
||||
}
|
||||
41
app/Models/Barobill/BarobillCardTransactionAmountLog.php
Normal file
41
app/Models/Barobill/BarobillCardTransactionAmountLog.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Barobill;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class BarobillCardTransactionAmountLog extends Model
|
||||
{
|
||||
protected $table = 'barobill_card_transaction_amount_logs';
|
||||
|
||||
public $timestamps = false;
|
||||
|
||||
protected $fillable = [
|
||||
'card_transaction_id',
|
||||
'original_unique_key',
|
||||
'before_supply_amount',
|
||||
'before_tax',
|
||||
'after_supply_amount',
|
||||
'after_tax',
|
||||
'modified_by',
|
||||
'modified_by_name',
|
||||
'ip_address',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'before_supply_amount' => 'decimal:2',
|
||||
'before_tax' => 'decimal:2',
|
||||
'after_supply_amount' => 'decimal:2',
|
||||
'after_tax' => 'decimal:2',
|
||||
];
|
||||
|
||||
// =========================================================================
|
||||
// 관계 정의
|
||||
// =========================================================================
|
||||
|
||||
public function cardTransaction(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(BarobillCardTransaction::class, 'card_transaction_id');
|
||||
}
|
||||
}
|
||||
61
app/Models/Barobill/BarobillCardTransactionHide.php
Normal file
61
app/Models/Barobill/BarobillCardTransactionHide.php
Normal file
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Barobill;
|
||||
|
||||
use App\Traits\BelongsToTenant;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class BarobillCardTransactionHide extends Model
|
||||
{
|
||||
use BelongsToTenant;
|
||||
|
||||
protected $table = 'barobill_card_transaction_hides';
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'original_unique_key',
|
||||
'card_num',
|
||||
'use_date',
|
||||
'approval_num',
|
||||
'original_amount',
|
||||
'merchant_name',
|
||||
'hidden_by',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'original_amount' => 'decimal:2',
|
||||
];
|
||||
|
||||
// =========================================================================
|
||||
// 헬퍼 메서드
|
||||
// =========================================================================
|
||||
|
||||
public static function getHiddenKeys(int $tenantId, string $startDate, string $endDate)
|
||||
{
|
||||
return static::where('tenant_id', $tenantId)
|
||||
->whereBetween('use_date', [$startDate, $endDate])
|
||||
->pluck('original_unique_key')
|
||||
->toArray();
|
||||
}
|
||||
|
||||
public static function hideTransaction(int $tenantId, string $uniqueKey, array $originalData, int $userId): self
|
||||
{
|
||||
return static::create([
|
||||
'tenant_id' => $tenantId,
|
||||
'original_unique_key' => $uniqueKey,
|
||||
'card_num' => $originalData['card_num'] ?? '',
|
||||
'use_date' => $originalData['use_date'] ?? '',
|
||||
'approval_num' => $originalData['approval_num'] ?? '',
|
||||
'original_amount' => $originalData['approval_amount'] ?? 0,
|
||||
'merchant_name' => $originalData['merchant_name'] ?? '',
|
||||
'hidden_by' => $userId,
|
||||
]);
|
||||
}
|
||||
|
||||
public static function restoreTransaction(int $tenantId, string $uniqueKey): bool
|
||||
{
|
||||
return static::where('tenant_id', $tenantId)
|
||||
->where('original_unique_key', $uniqueKey)
|
||||
->delete() > 0;
|
||||
}
|
||||
}
|
||||
74
app/Models/Barobill/BarobillCardTransactionSplit.php
Normal file
74
app/Models/Barobill/BarobillCardTransactionSplit.php
Normal file
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Barobill;
|
||||
|
||||
use App\Traits\BelongsToTenant;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class BarobillCardTransactionSplit extends Model
|
||||
{
|
||||
use BelongsToTenant;
|
||||
|
||||
protected $table = 'barobill_card_transaction_splits';
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'original_unique_key',
|
||||
'split_amount',
|
||||
'split_supply_amount',
|
||||
'split_tax',
|
||||
'account_code',
|
||||
'account_name',
|
||||
'deduction_type',
|
||||
'evidence_name',
|
||||
'description',
|
||||
'memo',
|
||||
'sort_order',
|
||||
'card_num',
|
||||
'use_dt',
|
||||
'use_date',
|
||||
'approval_num',
|
||||
'original_amount',
|
||||
'merchant_name',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'split_amount' => 'decimal:2',
|
||||
'split_supply_amount' => 'decimal:2',
|
||||
'split_tax' => 'decimal:2',
|
||||
'original_amount' => 'decimal:2',
|
||||
'sort_order' => 'integer',
|
||||
];
|
||||
|
||||
// =========================================================================
|
||||
// 관계 정의
|
||||
// =========================================================================
|
||||
|
||||
public function tenant(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(\App\Models\Tenants\Tenant::class);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 헬퍼 메서드
|
||||
// =========================================================================
|
||||
|
||||
public static function getByDateRange(int $tenantId, string $startDate, string $endDate)
|
||||
{
|
||||
return static::where('tenant_id', $tenantId)
|
||||
->whereBetween('use_date', [$startDate, $endDate])
|
||||
->orderBy('original_unique_key')
|
||||
->orderBy('sort_order')
|
||||
->get()
|
||||
->groupBy('original_unique_key');
|
||||
}
|
||||
|
||||
public static function getByUniqueKey(int $tenantId, string $uniqueKey)
|
||||
{
|
||||
return static::where('tenant_id', $tenantId)
|
||||
->where('original_unique_key', $uniqueKey)
|
||||
->orderBy('sort_order')
|
||||
->get();
|
||||
}
|
||||
}
|
||||
61
app/Models/Barobill/BarobillConfig.php
Normal file
61
app/Models/Barobill/BarobillConfig.php
Normal file
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Barobill;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class BarobillConfig extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
protected $table = 'barobill_configs';
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'environment',
|
||||
'cert_key',
|
||||
'corp_num',
|
||||
'base_url',
|
||||
'description',
|
||||
'is_active',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'is_active' => 'boolean',
|
||||
];
|
||||
|
||||
// =========================================================================
|
||||
// 헬퍼 메서드
|
||||
// =========================================================================
|
||||
|
||||
public static function getActiveTest(): ?self
|
||||
{
|
||||
return static::where('environment', 'test')->where('is_active', true)->first();
|
||||
}
|
||||
|
||||
public static function getActiveProduction(): ?self
|
||||
{
|
||||
return static::where('environment', 'production')->where('is_active', true)->first();
|
||||
}
|
||||
|
||||
public static function getActive(bool $isTestMode): ?self
|
||||
{
|
||||
return $isTestMode ? static::getActiveTest() : static::getActiveProduction();
|
||||
}
|
||||
|
||||
public function getEnvironmentLabelAttribute(): string
|
||||
{
|
||||
return $this->environment === 'test' ? '테스트' : '운영';
|
||||
}
|
||||
|
||||
public function getMaskedCertKeyAttribute(): string
|
||||
{
|
||||
$key = $this->cert_key;
|
||||
if (strlen($key) <= 8) {
|
||||
return str_repeat('*', strlen($key));
|
||||
}
|
||||
|
||||
return substr($key, 0, 4).'****'.substr($key, -4);
|
||||
}
|
||||
}
|
||||
82
app/Models/Barobill/BarobillMember.php
Normal file
82
app/Models/Barobill/BarobillMember.php
Normal file
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Barobill;
|
||||
|
||||
use App\Traits\BelongsToTenant;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class BarobillMember extends Model
|
||||
{
|
||||
use BelongsToTenant, SoftDeletes;
|
||||
|
||||
protected $table = 'barobill_members';
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'biz_no',
|
||||
'corp_name',
|
||||
'ceo_name',
|
||||
'addr',
|
||||
'biz_type',
|
||||
'biz_class',
|
||||
'barobill_id',
|
||||
'barobill_pwd',
|
||||
'manager_name',
|
||||
'manager_email',
|
||||
'manager_hp',
|
||||
'status',
|
||||
'server_mode',
|
||||
'last_sales_fetch_at',
|
||||
'last_purchases_fetch_at',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'barobill_pwd' => 'encrypted',
|
||||
'last_sales_fetch_at' => 'datetime',
|
||||
'last_purchases_fetch_at' => 'datetime',
|
||||
];
|
||||
|
||||
protected $hidden = [
|
||||
'barobill_pwd',
|
||||
];
|
||||
|
||||
// =========================================================================
|
||||
// 관계 정의
|
||||
// =========================================================================
|
||||
|
||||
public function tenant(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(\App\Models\Tenants\Tenant::class);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 접근자
|
||||
// =========================================================================
|
||||
|
||||
public function getFormattedBizNoAttribute(): string
|
||||
{
|
||||
$num = $this->biz_no;
|
||||
if (strlen($num) === 10) {
|
||||
return substr($num, 0, 3).'-'.substr($num, 3, 2).'-'.substr($num, 5);
|
||||
}
|
||||
|
||||
return $num ?? '';
|
||||
}
|
||||
|
||||
public function getStatusLabelAttribute(): string
|
||||
{
|
||||
return match ($this->status) {
|
||||
'active' => '활성',
|
||||
'inactive' => '비활성',
|
||||
'pending' => '대기',
|
||||
default => $this->status,
|
||||
};
|
||||
}
|
||||
|
||||
public function isTestMode(): bool
|
||||
{
|
||||
return $this->server_mode === 'test';
|
||||
}
|
||||
}
|
||||
53
app/Models/Barobill/BarobillMonthlySummary.php
Normal file
53
app/Models/Barobill/BarobillMonthlySummary.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Barobill;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class BarobillMonthlySummary extends Model
|
||||
{
|
||||
protected $table = 'barobill_monthly_summaries';
|
||||
|
||||
protected $fillable = [
|
||||
'member_id',
|
||||
'billing_month',
|
||||
'bank_account_fee',
|
||||
'card_fee',
|
||||
'hometax_fee',
|
||||
'subscription_total',
|
||||
'tax_invoice_count',
|
||||
'tax_invoice_amount',
|
||||
'usage_total',
|
||||
'grand_total',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'bank_account_fee' => 'integer',
|
||||
'card_fee' => 'integer',
|
||||
'hometax_fee' => 'integer',
|
||||
'subscription_total' => 'integer',
|
||||
'tax_invoice_count' => 'integer',
|
||||
'tax_invoice_amount' => 'integer',
|
||||
'usage_total' => 'integer',
|
||||
'grand_total' => 'integer',
|
||||
];
|
||||
|
||||
// =========================================================================
|
||||
// 관계 정의
|
||||
// =========================================================================
|
||||
|
||||
public function member(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(BarobillMember::class, 'member_id');
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 스코프
|
||||
// =========================================================================
|
||||
|
||||
public function scopeOfMonth($query, string $billingMonth)
|
||||
{
|
||||
return $query->where('billing_month', $billingMonth);
|
||||
}
|
||||
}
|
||||
82
app/Models/Barobill/BarobillPricingPolicy.php
Normal file
82
app/Models/Barobill/BarobillPricingPolicy.php
Normal file
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Barobill;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class BarobillPricingPolicy extends Model
|
||||
{
|
||||
protected $table = 'barobill_pricing_policies';
|
||||
|
||||
public const TYPE_CARD = 'card';
|
||||
|
||||
public const TYPE_TAX_INVOICE = 'tax_invoice';
|
||||
|
||||
public const TYPE_BANK_ACCOUNT = 'bank_account';
|
||||
|
||||
protected $fillable = [
|
||||
'service_type',
|
||||
'name',
|
||||
'description',
|
||||
'free_quota',
|
||||
'free_quota_unit',
|
||||
'additional_unit',
|
||||
'additional_unit_label',
|
||||
'additional_price',
|
||||
'is_active',
|
||||
'sort_order',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'free_quota' => 'integer',
|
||||
'additional_unit' => 'integer',
|
||||
'additional_price' => 'integer',
|
||||
'is_active' => 'boolean',
|
||||
'sort_order' => 'integer',
|
||||
];
|
||||
|
||||
// =========================================================================
|
||||
// 스코프
|
||||
// =========================================================================
|
||||
|
||||
public function scopeActive($query)
|
||||
{
|
||||
return $query->where('is_active', true);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 헬퍼 메서드
|
||||
// =========================================================================
|
||||
|
||||
public static function getByServiceType(string $serviceType): ?self
|
||||
{
|
||||
return static::active()->where('service_type', $serviceType)->first();
|
||||
}
|
||||
|
||||
public static function getAllActive()
|
||||
{
|
||||
return static::active()->orderBy('sort_order')->get();
|
||||
}
|
||||
|
||||
public function getServiceTypeLabelAttribute(): string
|
||||
{
|
||||
return match ($this->service_type) {
|
||||
self::TYPE_CARD => '카드조회',
|
||||
self::TYPE_TAX_INVOICE => '전자세금계산서',
|
||||
self::TYPE_BANK_ACCOUNT => '계좌조회',
|
||||
default => $this->service_type,
|
||||
};
|
||||
}
|
||||
|
||||
public function calculateBilling(int $usageCount): int
|
||||
{
|
||||
if ($usageCount <= $this->free_quota) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$excess = $usageCount - $this->free_quota;
|
||||
$units = (int) ceil($excess / max($this->additional_unit, 1));
|
||||
|
||||
return $units * $this->additional_price;
|
||||
}
|
||||
}
|
||||
76
app/Models/Barobill/BarobillSubscription.php
Normal file
76
app/Models/Barobill/BarobillSubscription.php
Normal file
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Barobill;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class BarobillSubscription extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
protected $table = 'barobill_subscriptions';
|
||||
|
||||
public const SERVICE_TYPES = ['bank_account', 'card', 'hometax'];
|
||||
|
||||
public const DEFAULT_MONTHLY_FEES = [
|
||||
'bank_account' => 10000,
|
||||
'card' => 10000,
|
||||
'hometax' => 0,
|
||||
];
|
||||
|
||||
protected $fillable = [
|
||||
'member_id',
|
||||
'service_type',
|
||||
'monthly_fee',
|
||||
'started_at',
|
||||
'ended_at',
|
||||
'is_active',
|
||||
'memo',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'monthly_fee' => 'integer',
|
||||
'started_at' => 'date',
|
||||
'ended_at' => 'date',
|
||||
'is_active' => 'boolean',
|
||||
];
|
||||
|
||||
// =========================================================================
|
||||
// 관계 정의
|
||||
// =========================================================================
|
||||
|
||||
public function member(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(BarobillMember::class, 'member_id');
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 스코프
|
||||
// =========================================================================
|
||||
|
||||
public function scopeActive($query)
|
||||
{
|
||||
return $query->where('is_active', true);
|
||||
}
|
||||
|
||||
public function scopeOfService($query, string $serviceType)
|
||||
{
|
||||
return $query->where('service_type', $serviceType);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 접근자
|
||||
// =========================================================================
|
||||
|
||||
public function getServiceTypeLabelAttribute(): string
|
||||
{
|
||||
return match ($this->service_type) {
|
||||
'bank_account' => '계좌조회',
|
||||
'card' => '카드조회',
|
||||
'hometax' => '홈택스',
|
||||
default => $this->service_type,
|
||||
};
|
||||
}
|
||||
}
|
||||
158
app/Models/Barobill/HometaxInvoice.php
Normal file
158
app/Models/Barobill/HometaxInvoice.php
Normal file
@@ -0,0 +1,158 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Barobill;
|
||||
|
||||
use App\Traits\BelongsToTenant;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class HometaxInvoice extends Model
|
||||
{
|
||||
use BelongsToTenant, SoftDeletes;
|
||||
|
||||
protected $table = 'hometax_invoices';
|
||||
|
||||
// 과세유형
|
||||
public const TAX_TYPE_TAXABLE = '01'; // 과세
|
||||
|
||||
public const TAX_TYPE_ZERO = '02'; // 영세
|
||||
|
||||
public const TAX_TYPE_EXEMPT = '03'; // 면세
|
||||
|
||||
// 영수/청구
|
||||
public const PURPOSE_TYPE_RECEIPT = '01'; // 영수
|
||||
|
||||
public const PURPOSE_TYPE_CLAIM = '02'; // 청구
|
||||
|
||||
// 발급유형
|
||||
public const ISSUE_TYPE_NORMAL = '01'; // 정발행
|
||||
|
||||
public const ISSUE_TYPE_REVERSE = '02'; // 역발행
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'nts_confirm_num',
|
||||
'invoice_type',
|
||||
'write_date',
|
||||
'issue_date',
|
||||
'send_date',
|
||||
'invoicer_corp_num',
|
||||
'invoicer_tax_reg_id',
|
||||
'invoicer_corp_name',
|
||||
'invoicer_ceo_name',
|
||||
'invoicer_addr',
|
||||
'invoicer_biz_type',
|
||||
'invoicer_biz_class',
|
||||
'invoicer_contact_id',
|
||||
'invoicee_corp_num',
|
||||
'invoicee_tax_reg_id',
|
||||
'invoicee_corp_name',
|
||||
'invoicee_ceo_name',
|
||||
'invoicee_addr',
|
||||
'invoicee_biz_type',
|
||||
'invoicee_biz_class',
|
||||
'invoicee_contact_id',
|
||||
'supply_amount',
|
||||
'tax_amount',
|
||||
'total_amount',
|
||||
'tax_type',
|
||||
'purpose_type',
|
||||
'issue_type',
|
||||
'is_modified',
|
||||
'original_nts_confirm_num',
|
||||
'modify_code',
|
||||
'remark1',
|
||||
'remark2',
|
||||
'remark3',
|
||||
'item_name',
|
||||
'item_count',
|
||||
'item_unit_price',
|
||||
'item_supply_amount',
|
||||
'item_tax_amount',
|
||||
'item_remark',
|
||||
'account_code',
|
||||
'account_name',
|
||||
'deduction_type',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'supply_amount' => 'integer',
|
||||
'tax_amount' => 'integer',
|
||||
'total_amount' => 'integer',
|
||||
'item_count' => 'integer',
|
||||
'item_unit_price' => 'integer',
|
||||
'item_supply_amount' => 'integer',
|
||||
'item_tax_amount' => 'integer',
|
||||
'is_modified' => 'boolean',
|
||||
'write_date' => 'date',
|
||||
'issue_date' => 'date',
|
||||
'send_date' => 'date',
|
||||
];
|
||||
|
||||
// =========================================================================
|
||||
// 관계 정의
|
||||
// =========================================================================
|
||||
|
||||
public function tenant(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(\App\Models\Tenants\Tenant::class);
|
||||
}
|
||||
|
||||
public function journals(): HasMany
|
||||
{
|
||||
return $this->hasMany(HometaxInvoiceJournal::class, 'hometax_invoice_id');
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 스코프
|
||||
// =========================================================================
|
||||
|
||||
public function scopeSales($query)
|
||||
{
|
||||
return $query->where('invoice_type', 'sales');
|
||||
}
|
||||
|
||||
public function scopePurchase($query)
|
||||
{
|
||||
return $query->where('invoice_type', 'purchase');
|
||||
}
|
||||
|
||||
public function scopePeriod($query, string $startDate, string $endDate)
|
||||
{
|
||||
return $query->whereBetween('write_date', [$startDate, $endDate]);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 접근자
|
||||
// =========================================================================
|
||||
|
||||
public function getTaxTypeNameAttribute(): string
|
||||
{
|
||||
return match ($this->tax_type) {
|
||||
self::TAX_TYPE_TAXABLE => '과세',
|
||||
self::TAX_TYPE_ZERO => '영세',
|
||||
self::TAX_TYPE_EXEMPT => '면세',
|
||||
default => $this->tax_type ?? '',
|
||||
};
|
||||
}
|
||||
|
||||
public function getPurposeTypeNameAttribute(): string
|
||||
{
|
||||
return match ($this->purpose_type) {
|
||||
self::PURPOSE_TYPE_RECEIPT => '영수',
|
||||
self::PURPOSE_TYPE_CLAIM => '청구',
|
||||
default => $this->purpose_type ?? '',
|
||||
};
|
||||
}
|
||||
|
||||
public function getIssueTypeNameAttribute(): string
|
||||
{
|
||||
return match ($this->issue_type) {
|
||||
self::ISSUE_TYPE_NORMAL => '정발행',
|
||||
self::ISSUE_TYPE_REVERSE => '역발행',
|
||||
default => $this->issue_type ?? '',
|
||||
};
|
||||
}
|
||||
}
|
||||
78
app/Models/Barobill/HometaxInvoiceJournal.php
Normal file
78
app/Models/Barobill/HometaxInvoiceJournal.php
Normal file
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Barobill;
|
||||
|
||||
use App\Traits\BelongsToTenant;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class HometaxInvoiceJournal extends Model
|
||||
{
|
||||
use BelongsToTenant;
|
||||
|
||||
protected $table = 'hometax_invoice_journals';
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'hometax_invoice_id',
|
||||
'nts_confirm_num',
|
||||
'dc_type',
|
||||
'account_code',
|
||||
'account_name',
|
||||
'debit_amount',
|
||||
'credit_amount',
|
||||
'description',
|
||||
'sort_order',
|
||||
'invoice_type',
|
||||
'write_date',
|
||||
'supply_amount',
|
||||
'tax_amount',
|
||||
'total_amount',
|
||||
'trading_partner_name',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'debit_amount' => 'integer',
|
||||
'credit_amount' => 'integer',
|
||||
'sort_order' => 'integer',
|
||||
'supply_amount' => 'integer',
|
||||
'tax_amount' => 'integer',
|
||||
'total_amount' => 'integer',
|
||||
'write_date' => 'date',
|
||||
];
|
||||
|
||||
// =========================================================================
|
||||
// 관계 정의
|
||||
// =========================================================================
|
||||
|
||||
public function tenant(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(\App\Models\Tenants\Tenant::class);
|
||||
}
|
||||
|
||||
public function invoice(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(HometaxInvoice::class, 'hometax_invoice_id');
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 헬퍼 메서드
|
||||
// =========================================================================
|
||||
|
||||
public static function getByInvoiceId(int $tenantId, int $invoiceId)
|
||||
{
|
||||
return static::where('tenant_id', $tenantId)
|
||||
->where('hometax_invoice_id', $invoiceId)
|
||||
->orderBy('sort_order')
|
||||
->get();
|
||||
}
|
||||
|
||||
public static function getJournaledInvoiceIds(int $tenantId, array $invoiceIds): array
|
||||
{
|
||||
return static::where('tenant_id', $tenantId)
|
||||
->whereIn('hometax_invoice_id', $invoiceIds)
|
||||
->distinct()
|
||||
->pluck('hometax_invoice_id')
|
||||
->toArray();
|
||||
}
|
||||
}
|
||||
@@ -103,7 +103,7 @@ public function fileable()
|
||||
*/
|
||||
public function getStoragePath(): string
|
||||
{
|
||||
return Storage::disk('tenant')->path($this->file_path);
|
||||
return $this->file_path;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -111,22 +111,38 @@ public function getStoragePath(): string
|
||||
*/
|
||||
public function exists(): bool
|
||||
{
|
||||
return Storage::disk('tenant')->exists($this->file_path);
|
||||
return Storage::disk('r2')->exists($this->file_path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get download response
|
||||
* Get download response (streaming from R2)
|
||||
*
|
||||
* @param bool $inline true = 브라우저에서 바로 표시 (이미지/PDF), false = 다운로드
|
||||
*/
|
||||
public function download()
|
||||
public function download(bool $inline = false)
|
||||
{
|
||||
if (! $this->exists()) {
|
||||
abort(404, 'File not found in storage');
|
||||
}
|
||||
|
||||
return response()->download(
|
||||
$this->getStoragePath(),
|
||||
$this->display_name ?? $this->original_name
|
||||
);
|
||||
$fileName = $this->display_name ?? $this->original_name;
|
||||
$mimeType = $this->mime_type ?? 'application/octet-stream';
|
||||
$disposition = $inline ? 'inline' : 'attachment';
|
||||
|
||||
// Stream from R2 (메모리에 전체 로드하지 않음)
|
||||
$stream = Storage::disk('r2')->readStream($this->file_path);
|
||||
|
||||
return response()->stream(function () use ($stream) {
|
||||
fpassthru($stream);
|
||||
if (is_resource($stream)) {
|
||||
fclose($stream);
|
||||
}
|
||||
}, 200, [
|
||||
'Content-Type' => $mimeType,
|
||||
'Content-Disposition' => $disposition . '; filename="' . $fileName . '"',
|
||||
'Content-Length' => $this->file_size,
|
||||
'Cache-Control' => 'private, max-age=3600',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -149,9 +165,9 @@ public function moveToFolder(Folder $folder): bool
|
||||
$this->stored_name ?? $this->file_name
|
||||
);
|
||||
|
||||
// Move physical file
|
||||
if (Storage::disk('tenant')->exists($this->file_path)) {
|
||||
Storage::disk('tenant')->move($this->file_path, $newPath);
|
||||
// Move physical file in R2
|
||||
if (Storage::disk('r2')->exists($this->file_path)) {
|
||||
Storage::disk('r2')->move($this->file_path, $newPath);
|
||||
}
|
||||
|
||||
// Update DB
|
||||
@@ -182,9 +198,9 @@ public function softDeleteFile(int $userId): void
|
||||
*/
|
||||
public function permanentDelete(): void
|
||||
{
|
||||
// Delete physical file
|
||||
// Delete physical file from R2
|
||||
if ($this->exists()) {
|
||||
Storage::disk('tenant')->delete($this->file_path);
|
||||
Storage::disk('r2')->delete($this->file_path);
|
||||
}
|
||||
|
||||
// Decrement tenant storage
|
||||
|
||||
154
app/Models/Equipment/Equipment.php
Normal file
154
app/Models/Equipment/Equipment.php
Normal file
@@ -0,0 +1,154 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Equipment;
|
||||
|
||||
use App\Models\Commons\File;
|
||||
use App\Traits\Auditable;
|
||||
use App\Traits\BelongsToTenant;
|
||||
use App\Traits\ModelTrait;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class Equipment extends Model
|
||||
{
|
||||
use Auditable, BelongsToTenant, ModelTrait, SoftDeletes;
|
||||
|
||||
protected $table = 'equipments';
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'equipment_code',
|
||||
'name',
|
||||
'equipment_type',
|
||||
'specification',
|
||||
'manufacturer',
|
||||
'model_name',
|
||||
'serial_no',
|
||||
'location',
|
||||
'production_line',
|
||||
'purchase_date',
|
||||
'install_date',
|
||||
'purchase_price',
|
||||
'useful_life',
|
||||
'status',
|
||||
'disposed_date',
|
||||
'manager_id',
|
||||
'sub_manager_id',
|
||||
'photo_path',
|
||||
'memo',
|
||||
'options',
|
||||
'is_active',
|
||||
'sort_order',
|
||||
'created_by',
|
||||
'updated_by',
|
||||
'deleted_by',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'purchase_date' => 'date',
|
||||
'install_date' => 'date',
|
||||
'disposed_date' => 'date',
|
||||
'purchase_price' => 'decimal:2',
|
||||
'is_active' => 'boolean',
|
||||
'options' => 'array',
|
||||
];
|
||||
|
||||
public function getOption(string $key, mixed $default = null): mixed
|
||||
{
|
||||
return data_get($this->options, $key, $default);
|
||||
}
|
||||
|
||||
public function setOption(string $key, mixed $value): self
|
||||
{
|
||||
$options = $this->options ?? [];
|
||||
data_set($options, $key, $value);
|
||||
$this->options = $options;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function manager(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(\App\Models\User::class, 'manager_id');
|
||||
}
|
||||
|
||||
public function subManager(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(\App\Models\User::class, 'sub_manager_id');
|
||||
}
|
||||
|
||||
public function canInspect(?int $userId = null): bool
|
||||
{
|
||||
if (! $userId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->manager_id === $userId || $this->sub_manager_id === $userId;
|
||||
}
|
||||
|
||||
public function inspectionTemplates(): HasMany
|
||||
{
|
||||
return $this->hasMany(EquipmentInspectionTemplate::class, 'equipment_id')->orderBy('sort_order');
|
||||
}
|
||||
|
||||
public function inspections(): HasMany
|
||||
{
|
||||
return $this->hasMany(EquipmentInspection::class, 'equipment_id');
|
||||
}
|
||||
|
||||
public function repairs(): HasMany
|
||||
{
|
||||
return $this->hasMany(EquipmentRepair::class, 'equipment_id');
|
||||
}
|
||||
|
||||
public function photos(): HasMany
|
||||
{
|
||||
return $this->hasMany(File::class, 'document_id')
|
||||
->where('document_type', 'equipment')
|
||||
->orderBy('id');
|
||||
}
|
||||
|
||||
public function processes(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(\App\Models\Process::class, 'equipment_process')
|
||||
->withPivot('is_primary')
|
||||
->withTimestamps();
|
||||
}
|
||||
|
||||
public function scopeByLine($query, string $line)
|
||||
{
|
||||
return $query->where('production_line', $line);
|
||||
}
|
||||
|
||||
public function scopeByType($query, string $type)
|
||||
{
|
||||
return $query->where('equipment_type', $type);
|
||||
}
|
||||
|
||||
public function scopeByStatus($query, string $status)
|
||||
{
|
||||
return $query->where('status', $status);
|
||||
}
|
||||
|
||||
public static function getEquipmentTypes(): array
|
||||
{
|
||||
return ['포밍기', '미싱기', '샤링기', 'V컷팅기', '절곡기', '프레스', '드릴', '기타'];
|
||||
}
|
||||
|
||||
public static function getProductionLines(): array
|
||||
{
|
||||
return ['스라트', '스크린', '절곡', '기타'];
|
||||
}
|
||||
|
||||
public static function getStatuses(): array
|
||||
{
|
||||
return [
|
||||
'active' => '가동',
|
||||
'idle' => '유휴',
|
||||
'disposed' => '폐기',
|
||||
];
|
||||
}
|
||||
}
|
||||
43
app/Models/Equipment/EquipmentInspection.php
Normal file
43
app/Models/Equipment/EquipmentInspection.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Equipment;
|
||||
|
||||
use App\Traits\Auditable;
|
||||
use App\Traits\BelongsToTenant;
|
||||
use App\Traits\ModelTrait;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class EquipmentInspection extends Model
|
||||
{
|
||||
use Auditable, BelongsToTenant, ModelTrait;
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'equipment_id',
|
||||
'inspection_cycle',
|
||||
'year_month',
|
||||
'overall_judgment',
|
||||
'inspector_id',
|
||||
'repair_note',
|
||||
'issue_note',
|
||||
'created_by',
|
||||
'updated_by',
|
||||
];
|
||||
|
||||
public function equipment(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Equipment::class, 'equipment_id');
|
||||
}
|
||||
|
||||
public function inspector(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(\App\Models\User::class, 'inspector_id');
|
||||
}
|
||||
|
||||
public function details(): HasMany
|
||||
{
|
||||
return $this->hasMany(EquipmentInspectionDetail::class, 'inspection_id');
|
||||
}
|
||||
}
|
||||
55
app/Models/Equipment/EquipmentInspectionDetail.php
Normal file
55
app/Models/Equipment/EquipmentInspectionDetail.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Equipment;
|
||||
|
||||
use App\Traits\ModelTrait;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class EquipmentInspectionDetail extends Model
|
||||
{
|
||||
use ModelTrait;
|
||||
|
||||
protected $fillable = [
|
||||
'inspection_id',
|
||||
'template_item_id',
|
||||
'check_date',
|
||||
'result',
|
||||
'note',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'check_date' => 'date',
|
||||
];
|
||||
|
||||
public function inspection(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(EquipmentInspection::class, 'inspection_id');
|
||||
}
|
||||
|
||||
public function templateItem(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(EquipmentInspectionTemplate::class, 'template_item_id');
|
||||
}
|
||||
|
||||
public static function getNextResult(?string $current): ?string
|
||||
{
|
||||
return match ($current) {
|
||||
null, '' => 'good',
|
||||
'good' => 'bad',
|
||||
'bad' => 'repaired',
|
||||
'repaired' => null,
|
||||
default => 'good',
|
||||
};
|
||||
}
|
||||
|
||||
public static function getResultSymbol(?string $result): string
|
||||
{
|
||||
return match ($result) {
|
||||
'good' => '○',
|
||||
'bad' => 'X',
|
||||
'repaired' => '△',
|
||||
default => '',
|
||||
};
|
||||
}
|
||||
}
|
||||
42
app/Models/Equipment/EquipmentInspectionTemplate.php
Normal file
42
app/Models/Equipment/EquipmentInspectionTemplate.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Equipment;
|
||||
|
||||
use App\Traits\Auditable;
|
||||
use App\Traits\BelongsToTenant;
|
||||
use App\Traits\ModelTrait;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class EquipmentInspectionTemplate extends Model
|
||||
{
|
||||
use Auditable, BelongsToTenant, ModelTrait;
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'equipment_id',
|
||||
'inspection_cycle',
|
||||
'item_no',
|
||||
'check_point',
|
||||
'check_item',
|
||||
'check_timing',
|
||||
'check_frequency',
|
||||
'check_method',
|
||||
'sort_order',
|
||||
'is_active',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'is_active' => 'boolean',
|
||||
];
|
||||
|
||||
public function equipment(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Equipment::class, 'equipment_id');
|
||||
}
|
||||
|
||||
public function scopeByCycle($query, string $cycle)
|
||||
{
|
||||
return $query->where('inspection_cycle', $cycle);
|
||||
}
|
||||
}
|
||||
31
app/Models/Equipment/EquipmentProcess.php
Normal file
31
app/Models/Equipment/EquipmentProcess.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Equipment;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class EquipmentProcess extends Model
|
||||
{
|
||||
protected $table = 'equipment_process';
|
||||
|
||||
protected $fillable = [
|
||||
'equipment_id',
|
||||
'process_id',
|
||||
'is_primary',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'is_primary' => 'boolean',
|
||||
];
|
||||
|
||||
public function equipment(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Equipment::class, 'equipment_id');
|
||||
}
|
||||
|
||||
public function process(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(\App\Models\Process::class, 'process_id');
|
||||
}
|
||||
}
|
||||
62
app/Models/Equipment/EquipmentRepair.php
Normal file
62
app/Models/Equipment/EquipmentRepair.php
Normal file
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Equipment;
|
||||
|
||||
use App\Traits\Auditable;
|
||||
use App\Traits\BelongsToTenant;
|
||||
use App\Traits\ModelTrait;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class EquipmentRepair extends Model
|
||||
{
|
||||
use Auditable, BelongsToTenant, ModelTrait, SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'equipment_id',
|
||||
'repair_date',
|
||||
'repair_type',
|
||||
'repair_hours',
|
||||
'description',
|
||||
'cost',
|
||||
'vendor',
|
||||
'repaired_by',
|
||||
'memo',
|
||||
'options',
|
||||
'created_by',
|
||||
'updated_by',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'repair_date' => 'date',
|
||||
'repair_hours' => 'decimal:1',
|
||||
'cost' => 'decimal:2',
|
||||
'options' => 'array',
|
||||
];
|
||||
|
||||
public function getOption(string $key, mixed $default = null): mixed
|
||||
{
|
||||
return data_get($this->options, $key, $default);
|
||||
}
|
||||
|
||||
public function setOption(string $key, mixed $value): self
|
||||
{
|
||||
$options = $this->options ?? [];
|
||||
data_set($options, $key, $value);
|
||||
$this->options = $options;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function equipment(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Equipment::class, 'equipment_id');
|
||||
}
|
||||
|
||||
public function repairer(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(\App\Models\User::class, 'repaired_by');
|
||||
}
|
||||
}
|
||||
57
app/Models/Qualitys/AuditChecklist.php
Normal file
57
app/Models/Qualitys/AuditChecklist.php
Normal file
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Qualitys;
|
||||
|
||||
use App\Traits\Auditable;
|
||||
use App\Traits\BelongsToTenant;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class AuditChecklist extends Model
|
||||
{
|
||||
use Auditable, BelongsToTenant, SoftDeletes;
|
||||
|
||||
protected $table = 'audit_checklists';
|
||||
|
||||
const STATUS_DRAFT = 'draft';
|
||||
|
||||
const STATUS_IN_PROGRESS = 'in_progress';
|
||||
|
||||
const STATUS_COMPLETED = 'completed';
|
||||
|
||||
const TYPE_STANDARD_MANUAL = 'standard_manual';
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'year',
|
||||
'quarter',
|
||||
'type',
|
||||
'status',
|
||||
'options',
|
||||
'created_by',
|
||||
'updated_by',
|
||||
'deleted_by',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'year' => 'integer',
|
||||
'quarter' => 'integer',
|
||||
'options' => 'array',
|
||||
];
|
||||
|
||||
public function categories(): HasMany
|
||||
{
|
||||
return $this->hasMany(AuditChecklistCategory::class, 'checklist_id')->orderBy('sort_order');
|
||||
}
|
||||
|
||||
public function isDraft(): bool
|
||||
{
|
||||
return $this->status === self::STATUS_DRAFT;
|
||||
}
|
||||
|
||||
public function isCompleted(): bool
|
||||
{
|
||||
return $this->status === self::STATUS_COMPLETED;
|
||||
}
|
||||
}
|
||||
35
app/Models/Qualitys/AuditChecklistCategory.php
Normal file
35
app/Models/Qualitys/AuditChecklistCategory.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Qualitys;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class AuditChecklistCategory extends Model
|
||||
{
|
||||
protected $table = 'audit_checklist_categories';
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'checklist_id',
|
||||
'title',
|
||||
'sort_order',
|
||||
'options',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'sort_order' => 'integer',
|
||||
'options' => 'array',
|
||||
];
|
||||
|
||||
public function checklist(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(AuditChecklist::class, 'checklist_id');
|
||||
}
|
||||
|
||||
public function items(): HasMany
|
||||
{
|
||||
return $this->hasMany(AuditChecklistItem::class, 'category_id')->orderBy('sort_order');
|
||||
}
|
||||
}
|
||||
47
app/Models/Qualitys/AuditChecklistItem.php
Normal file
47
app/Models/Qualitys/AuditChecklistItem.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Qualitys;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class AuditChecklistItem extends Model
|
||||
{
|
||||
protected $table = 'audit_checklist_items';
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'category_id',
|
||||
'name',
|
||||
'description',
|
||||
'is_completed',
|
||||
'completed_at',
|
||||
'completed_by',
|
||||
'sort_order',
|
||||
'options',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'is_completed' => 'boolean',
|
||||
'completed_at' => 'datetime',
|
||||
'sort_order' => 'integer',
|
||||
'options' => 'array',
|
||||
];
|
||||
|
||||
public function category(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(AuditChecklistCategory::class, 'category_id');
|
||||
}
|
||||
|
||||
public function completedByUser(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'completed_by');
|
||||
}
|
||||
|
||||
public function standardDocuments(): HasMany
|
||||
{
|
||||
return $this->hasMany(AuditStandardDocument::class, 'checklist_item_id');
|
||||
}
|
||||
}
|
||||
37
app/Models/Qualitys/AuditStandardDocument.php
Normal file
37
app/Models/Qualitys/AuditStandardDocument.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Qualitys;
|
||||
|
||||
use App\Models\Documents\Document;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class AuditStandardDocument extends Model
|
||||
{
|
||||
protected $table = 'audit_standard_documents';
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'checklist_item_id',
|
||||
'title',
|
||||
'version',
|
||||
'date',
|
||||
'document_id',
|
||||
'options',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'date' => 'date',
|
||||
'options' => 'array',
|
||||
];
|
||||
|
||||
public function checklistItem(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(AuditChecklistItem::class, 'checklist_item_id');
|
||||
}
|
||||
|
||||
public function document(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Document::class);
|
||||
}
|
||||
}
|
||||
76
app/Models/Qualitys/ChecklistTemplate.php
Normal file
76
app/Models/Qualitys/ChecklistTemplate.php
Normal file
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Qualitys;
|
||||
|
||||
use App\Models\Commons\File;
|
||||
use App\Traits\Auditable;
|
||||
use App\Traits\BelongsToTenant;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class ChecklistTemplate extends Model
|
||||
{
|
||||
use Auditable, BelongsToTenant, SoftDeletes;
|
||||
|
||||
protected $table = 'checklist_templates';
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'name',
|
||||
'type',
|
||||
'categories',
|
||||
'options',
|
||||
'created_by',
|
||||
'updated_by',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'categories' => 'array',
|
||||
'options' => 'array',
|
||||
];
|
||||
|
||||
public function creator(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(\App\Models\Members\User::class, 'created_by');
|
||||
}
|
||||
|
||||
public function updater(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(\App\Models\Members\User::class, 'updated_by');
|
||||
}
|
||||
|
||||
/**
|
||||
* 점검항목별 연결 파일 (files 테이블 polymorphic)
|
||||
* document_type = 'checklist_template', document_id = this.id
|
||||
* field_key = sub_item_id (e.g. 'cat-1-1')
|
||||
*/
|
||||
public function documents(): MorphMany
|
||||
{
|
||||
return $this->morphMany(File::class, 'document', 'document_type', 'document_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 항목의 파일 조회
|
||||
*/
|
||||
public function documentsForItem(string $subItemId)
|
||||
{
|
||||
return $this->documents()->where('field_key', $subItemId);
|
||||
}
|
||||
|
||||
/**
|
||||
* categories JSON에서 모든 sub_item_id 추출
|
||||
*/
|
||||
public function getAllSubItemIds(): array
|
||||
{
|
||||
$ids = [];
|
||||
foreach ($this->categories ?? [] as $category) {
|
||||
foreach ($category['subItems'] ?? [] as $subItem) {
|
||||
$ids[] = $subItem['id'];
|
||||
}
|
||||
}
|
||||
|
||||
return $ids;
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,8 @@ class QualityDocumentLocation extends Model
|
||||
|
||||
const STATUS_PENDING = 'pending';
|
||||
|
||||
const STATUS_IN_PROGRESS = 'in_progress';
|
||||
|
||||
const STATUS_COMPLETED = 'completed';
|
||||
|
||||
protected $fillable = [
|
||||
@@ -24,10 +26,12 @@ class QualityDocumentLocation extends Model
|
||||
'inspection_data',
|
||||
'document_id',
|
||||
'inspection_status',
|
||||
'options',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'inspection_data' => 'array',
|
||||
'options' => 'array',
|
||||
];
|
||||
|
||||
public function qualityDocument()
|
||||
|
||||
@@ -15,16 +15,22 @@ class AccountCode extends Model
|
||||
'code',
|
||||
'name',
|
||||
'category',
|
||||
'sub_category',
|
||||
'parent_code',
|
||||
'depth',
|
||||
'department_type',
|
||||
'description',
|
||||
'sort_order',
|
||||
'is_active',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'depth' => 'integer',
|
||||
'sort_order' => 'integer',
|
||||
'is_active' => 'boolean',
|
||||
];
|
||||
|
||||
// Categories
|
||||
// Categories (대분류)
|
||||
public const CATEGORY_ASSET = 'asset';
|
||||
public const CATEGORY_LIABILITY = 'liability';
|
||||
public const CATEGORY_CAPITAL = 'capital';
|
||||
@@ -39,6 +45,36 @@ class AccountCode extends Model
|
||||
self::CATEGORY_EXPENSE => '비용',
|
||||
];
|
||||
|
||||
// Sub-categories (중분류)
|
||||
public const SUB_CATEGORIES = [
|
||||
'current_asset' => '유동자산',
|
||||
'fixed_asset' => '비유동자산',
|
||||
'current_liability' => '유동부채',
|
||||
'long_term_liability' => '비유동부채',
|
||||
'capital' => '자본',
|
||||
'sales_revenue' => '매출',
|
||||
'other_revenue' => '영업외수익',
|
||||
'cogs' => '매출원가',
|
||||
'selling_admin' => '판매비와관리비',
|
||||
'other_expense' => '영업외비용',
|
||||
];
|
||||
|
||||
// Department types (부문)
|
||||
public const DEPT_COMMON = 'common';
|
||||
public const DEPT_MANUFACTURING = 'manufacturing';
|
||||
public const DEPT_ADMIN = 'admin';
|
||||
|
||||
public const DEPARTMENT_TYPES = [
|
||||
self::DEPT_COMMON => '공통',
|
||||
self::DEPT_MANUFACTURING => '제조',
|
||||
self::DEPT_ADMIN => '관리',
|
||||
];
|
||||
|
||||
// Depth levels (계층)
|
||||
public const DEPTH_MAJOR = 1;
|
||||
public const DEPTH_MIDDLE = 2;
|
||||
public const DEPTH_MINOR = 3;
|
||||
|
||||
/**
|
||||
* 활성 계정과목만 조회
|
||||
*/
|
||||
@@ -46,4 +82,21 @@ public function scopeActive(Builder $query): Builder
|
||||
{
|
||||
return $query->where('is_active', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 소분류(입력 가능 계정)만 조회
|
||||
*/
|
||||
public function scopeSelectable(Builder $query): Builder
|
||||
{
|
||||
return $query->where('depth', self::DEPTH_MINOR);
|
||||
}
|
||||
|
||||
/**
|
||||
* 하위 계정과목 관계
|
||||
*/
|
||||
public function children()
|
||||
{
|
||||
return $this->hasMany(self::class, 'parent_code', 'code')
|
||||
->where('tenant_id', $this->tenant_id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,22 +39,35 @@ class Approval extends Model
|
||||
protected $casts = [
|
||||
'content' => 'array',
|
||||
'attachments' => 'array',
|
||||
'rejection_history' => 'array',
|
||||
'is_urgent' => 'boolean',
|
||||
'drafted_at' => 'datetime',
|
||||
'completed_at' => 'datetime',
|
||||
'drafter_read_at' => 'datetime',
|
||||
'current_step' => 'integer',
|
||||
'resubmit_count' => 'integer',
|
||||
];
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'document_number',
|
||||
'form_id',
|
||||
'line_id',
|
||||
'title',
|
||||
'content',
|
||||
'body',
|
||||
'status',
|
||||
'is_urgent',
|
||||
'drafter_id',
|
||||
'department_id',
|
||||
'drafted_at',
|
||||
'completed_at',
|
||||
'drafter_read_at',
|
||||
'current_step',
|
||||
'resubmit_count',
|
||||
'rejection_history',
|
||||
'recall_reason',
|
||||
'parent_doc_id',
|
||||
'attachments',
|
||||
'linkable_type',
|
||||
'linkable_id',
|
||||
@@ -66,6 +79,7 @@ class Approval extends Model
|
||||
protected $attributes = [
|
||||
'status' => 'draft',
|
||||
'current_step' => 0,
|
||||
'resubmit_count' => 0,
|
||||
];
|
||||
|
||||
// =========================================================================
|
||||
@@ -82,12 +96,15 @@ class Approval extends Model
|
||||
|
||||
public const STATUS_CANCELLED = 'cancelled'; // 회수/취소
|
||||
|
||||
public const STATUS_ON_HOLD = 'on_hold'; // 보류
|
||||
|
||||
public const STATUSES = [
|
||||
self::STATUS_DRAFT,
|
||||
self::STATUS_PENDING,
|
||||
self::STATUS_APPROVED,
|
||||
self::STATUS_REJECTED,
|
||||
self::STATUS_CANCELLED,
|
||||
self::STATUS_ON_HOLD,
|
||||
];
|
||||
|
||||
// =========================================================================
|
||||
@@ -102,6 +119,14 @@ public function form(): BelongsTo
|
||||
return $this->belongsTo(ApprovalForm::class, 'form_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 결재선 템플릿
|
||||
*/
|
||||
public function line(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(ApprovalLine::class, 'line_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 기안자
|
||||
*/
|
||||
@@ -110,6 +135,30 @@ public function drafter(): BelongsTo
|
||||
return $this->belongsTo(User::class, 'drafter_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 기안 부서
|
||||
*/
|
||||
public function department(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Department::class, 'department_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 원본 문서 (복사 재기안 시)
|
||||
*/
|
||||
public function parentDocument(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(self::class, 'parent_doc_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 하위 문서들 (이 문서에서 복사된 문서들)
|
||||
*/
|
||||
public function childDocuments(): HasMany
|
||||
{
|
||||
return $this->hasMany(self::class, 'parent_doc_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 결재 단계들
|
||||
*/
|
||||
@@ -207,11 +256,19 @@ public function scopeRejected($query)
|
||||
}
|
||||
|
||||
/**
|
||||
* 완료됨 (승인 또는 반려)
|
||||
* 완료됨 (승인, 반려, 회수)
|
||||
*/
|
||||
public function scopeCompleted($query)
|
||||
{
|
||||
return $query->whereIn('status', [self::STATUS_APPROVED, self::STATUS_REJECTED]);
|
||||
return $query->whereIn('status', [self::STATUS_APPROVED, self::STATUS_REJECTED, self::STATUS_CANCELLED]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 보류 상태
|
||||
*/
|
||||
public function scopeOnHold($query)
|
||||
{
|
||||
return $query->where('status', self::STATUS_ON_HOLD);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -227,19 +284,19 @@ public function scopeByDrafter($query, int $userId)
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 수정 가능 여부 (임시저장 상태만)
|
||||
* 수정 가능 여부 (임시저장 또는 반려 상태)
|
||||
*/
|
||||
public function isEditable(): bool
|
||||
{
|
||||
return $this->status === self::STATUS_DRAFT;
|
||||
return in_array($this->status, [self::STATUS_DRAFT, self::STATUS_REJECTED]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 상신 가능 여부
|
||||
* 상신 가능 여부 (임시저장 또는 반려 상태 = 재상신)
|
||||
*/
|
||||
public function isSubmittable(): bool
|
||||
{
|
||||
return $this->status === self::STATUS_DRAFT;
|
||||
return in_array($this->status, [self::STATUS_DRAFT, self::STATUS_REJECTED]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -251,21 +308,60 @@ public function isActionable(): bool
|
||||
}
|
||||
|
||||
/**
|
||||
* 회수 가능 여부 (기안자만, 진행중 상태)
|
||||
* 회수 가능 여부 (기안자만, 진행중 또는 보류 상태)
|
||||
*/
|
||||
public function isCancellable(): bool
|
||||
{
|
||||
return in_array($this->status, [self::STATUS_PENDING, self::STATUS_ON_HOLD]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 보류 가능 여부 (진행중 상태만)
|
||||
*/
|
||||
public function isHoldable(): bool
|
||||
{
|
||||
return $this->status === self::STATUS_PENDING;
|
||||
}
|
||||
|
||||
/**
|
||||
* 삭제 가능 여부 (임시저장만)
|
||||
* 보류 해제 가능 여부 (보류 상태만)
|
||||
*/
|
||||
public function isHoldReleasable(): bool
|
||||
{
|
||||
return $this->status === self::STATUS_ON_HOLD;
|
||||
}
|
||||
|
||||
/**
|
||||
* 복사 재기안 가능 여부 (완료/반려/회수 상태)
|
||||
*/
|
||||
public function isCopyable(): bool
|
||||
{
|
||||
return in_array($this->status, [self::STATUS_APPROVED, self::STATUS_REJECTED, self::STATUS_CANCELLED]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 삭제 가능 여부
|
||||
* 일반 사용자: 임시저장만
|
||||
* 관리자: 별도 isDeletableBy 사용
|
||||
*/
|
||||
public function isDeletable(): bool
|
||||
{
|
||||
return $this->status === self::STATUS_DRAFT;
|
||||
}
|
||||
|
||||
/**
|
||||
* 사용자 기준 삭제 가능 여부
|
||||
* 기안자: 임시저장/반려만 삭제 가능
|
||||
*/
|
||||
public function isDeletableBy(int $userId): bool
|
||||
{
|
||||
if ($this->drafter_id !== $userId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return in_array($this->status, [self::STATUS_DRAFT, self::STATUS_REJECTED]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 상태 라벨
|
||||
*/
|
||||
@@ -277,10 +373,27 @@ public function getStatusLabelAttribute(): string
|
||||
self::STATUS_APPROVED => '완료',
|
||||
self::STATUS_REJECTED => '반려',
|
||||
self::STATUS_CANCELLED => '회수',
|
||||
self::STATUS_ON_HOLD => '보류',
|
||||
default => $this->status,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 상태 색상 (UI 배지용)
|
||||
*/
|
||||
public function getStatusColorAttribute(): string
|
||||
{
|
||||
return match ($this->status) {
|
||||
self::STATUS_DRAFT => 'gray',
|
||||
self::STATUS_PENDING => 'blue',
|
||||
self::STATUS_APPROVED => 'green',
|
||||
self::STATUS_REJECTED => 'red',
|
||||
self::STATUS_CANCELLED => 'yellow',
|
||||
self::STATUS_ON_HOLD => 'orange',
|
||||
default => 'gray',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 결재자 확인
|
||||
*/
|
||||
|
||||
77
app/Models/Tenants/ApprovalDelegation.php
Normal file
77
app/Models/Tenants/ApprovalDelegation.php
Normal file
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Tenants;
|
||||
|
||||
use App\Models\Members\User;
|
||||
use App\Traits\Auditable;
|
||||
use App\Traits\BelongsToTenant;
|
||||
use App\Traits\ModelTrait;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class ApprovalDelegation extends Model
|
||||
{
|
||||
use Auditable, BelongsToTenant, ModelTrait, SoftDeletes;
|
||||
|
||||
protected $table = 'approval_delegations';
|
||||
|
||||
protected $casts = [
|
||||
'form_ids' => 'array',
|
||||
'start_date' => 'date',
|
||||
'end_date' => 'date',
|
||||
'notify_delegator' => 'boolean',
|
||||
'is_active' => 'boolean',
|
||||
];
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'delegator_id',
|
||||
'delegate_id',
|
||||
'start_date',
|
||||
'end_date',
|
||||
'form_ids',
|
||||
'notify_delegator',
|
||||
'is_active',
|
||||
'reason',
|
||||
'created_by',
|
||||
];
|
||||
|
||||
// =========================================================================
|
||||
// 관계 정의
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 위임자 (원래 결재자)
|
||||
*/
|
||||
public function delegator(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'delegator_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 대리자 (대신 결재하는 사람)
|
||||
*/
|
||||
public function delegate(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'delegate_id');
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 스코프
|
||||
// =========================================================================
|
||||
|
||||
public function scopeForDelegator($query, int $userId)
|
||||
{
|
||||
return $query->where('delegator_id', $userId);
|
||||
}
|
||||
|
||||
public function scopeCurrentlyActive($query)
|
||||
{
|
||||
$today = now()->toDateString();
|
||||
|
||||
return $query->active()
|
||||
->where('start_date', '<=', $today)
|
||||
->where('end_date', '>=', $today);
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
use App\Models\Members\User;
|
||||
use App\Traits\Auditable;
|
||||
use App\Traits\BelongsToTenant;
|
||||
use App\Traits\ModelTrait;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
@@ -26,7 +27,7 @@
|
||||
*/
|
||||
class ApprovalForm extends Model
|
||||
{
|
||||
use Auditable, BelongsToTenant, SoftDeletes;
|
||||
use Auditable, BelongsToTenant, ModelTrait, SoftDeletes;
|
||||
|
||||
protected $table = 'approval_forms';
|
||||
|
||||
@@ -41,6 +42,7 @@ class ApprovalForm extends Model
|
||||
'code',
|
||||
'category',
|
||||
'template',
|
||||
'body_template',
|
||||
'is_active',
|
||||
'created_by',
|
||||
'updated_by',
|
||||
@@ -99,14 +101,6 @@ public function updater(): BelongsTo
|
||||
// 스코프
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 활성 양식만
|
||||
*/
|
||||
public function scopeActive($query)
|
||||
{
|
||||
return $query->where('is_active', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 카테고리
|
||||
*/
|
||||
|
||||
@@ -4,13 +4,16 @@
|
||||
|
||||
use App\Models\Members\User;
|
||||
use App\Traits\Auditable;
|
||||
use App\Traits\BelongsToTenant;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
/**
|
||||
* 결재 단계 모델
|
||||
*
|
||||
* @property int $id
|
||||
* @property int $tenant_id
|
||||
* @property int $approval_id
|
||||
* @property int $step_order
|
||||
* @property string $step_type
|
||||
@@ -23,17 +26,20 @@
|
||||
*/
|
||||
class ApprovalStep extends Model
|
||||
{
|
||||
use Auditable;
|
||||
use Auditable, BelongsToTenant, SoftDeletes;
|
||||
|
||||
protected $table = 'approval_steps';
|
||||
|
||||
protected $casts = [
|
||||
'step_order' => 'integer',
|
||||
'parallel_group' => 'integer',
|
||||
'acted_at' => 'datetime',
|
||||
'is_read' => 'boolean',
|
||||
'read_at' => 'datetime',
|
||||
];
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'approval_id',
|
||||
'step_order',
|
||||
'step_type',
|
||||
@@ -41,8 +47,14 @@ class ApprovalStep extends Model
|
||||
'status',
|
||||
'comment',
|
||||
'acted_at',
|
||||
'acted_by',
|
||||
'is_read',
|
||||
'read_at',
|
||||
'parallel_group',
|
||||
'approval_type',
|
||||
'approver_name',
|
||||
'approver_department',
|
||||
'approver_position',
|
||||
];
|
||||
|
||||
protected $attributes = [
|
||||
@@ -62,11 +74,14 @@ class ApprovalStep extends Model
|
||||
|
||||
public const STATUS_SKIPPED = 'skipped'; // 건너뜀
|
||||
|
||||
public const STATUS_ON_HOLD = 'on_hold'; // 보류
|
||||
|
||||
public const STATUSES = [
|
||||
self::STATUS_PENDING,
|
||||
self::STATUS_APPROVED,
|
||||
self::STATUS_REJECTED,
|
||||
self::STATUS_SKIPPED,
|
||||
self::STATUS_ON_HOLD,
|
||||
];
|
||||
|
||||
// =========================================================================
|
||||
@@ -89,6 +104,14 @@ public function approver(): BelongsTo
|
||||
return $this->belongsTo(User::class, 'approver_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 실제 처리자 (위임 결재 시 대리자)
|
||||
*/
|
||||
public function actedBy(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'acted_by');
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 스코프
|
||||
// =========================================================================
|
||||
@@ -164,6 +187,7 @@ public function getStatusLabelAttribute(): string
|
||||
self::STATUS_APPROVED => '승인',
|
||||
self::STATUS_REJECTED => '반려',
|
||||
self::STATUS_SKIPPED => '건너뜀',
|
||||
self::STATUS_ON_HOLD => '보류',
|
||||
default => $this->status,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2,12 +2,15 @@
|
||||
|
||||
namespace App\Models\Tenants;
|
||||
|
||||
use App\Traits\BelongsToTenant;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Support\Facades\Crypt;
|
||||
|
||||
class BarobillSetting extends Model
|
||||
{
|
||||
use BelongsToTenant;
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'corp_num',
|
||||
@@ -23,6 +26,10 @@ class BarobillSetting extends Model
|
||||
'contact_tel',
|
||||
'is_active',
|
||||
'auto_issue',
|
||||
'use_tax_invoice',
|
||||
'use_bank_account',
|
||||
'use_card_usage',
|
||||
'use_hometax',
|
||||
'verified_at',
|
||||
'created_by',
|
||||
'updated_by',
|
||||
@@ -31,6 +38,10 @@ class BarobillSetting extends Model
|
||||
protected $casts = [
|
||||
'is_active' => 'boolean',
|
||||
'auto_issue' => 'boolean',
|
||||
'use_tax_invoice' => 'boolean',
|
||||
'use_bank_account' => 'boolean',
|
||||
'use_card_usage' => 'boolean',
|
||||
'use_hometax' => 'boolean',
|
||||
'verified_at' => 'datetime',
|
||||
];
|
||||
|
||||
@@ -129,4 +140,26 @@ public function getFormattedCorpNumAttribute(): string
|
||||
|
||||
return $num;
|
||||
}
|
||||
|
||||
/**
|
||||
* 활성화된 서비스 목록
|
||||
*/
|
||||
public function getActiveServicesAttribute(): array
|
||||
{
|
||||
$services = [];
|
||||
if ($this->use_tax_invoice) {
|
||||
$services[] = 'tax_invoice';
|
||||
}
|
||||
if ($this->use_bank_account) {
|
||||
$services[] = 'bank_account';
|
||||
}
|
||||
if ($this->use_card_usage) {
|
||||
$services[] = 'card_usage';
|
||||
}
|
||||
if ($this->use_hometax) {
|
||||
$services[] = 'hometax';
|
||||
}
|
||||
|
||||
return $services;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,8 @@ class ExpenseAccount extends Model
|
||||
'payment_method',
|
||||
'card_no',
|
||||
'loan_id',
|
||||
'journal_entry_id',
|
||||
'journal_entry_line_id',
|
||||
'created_by',
|
||||
'updated_by',
|
||||
'deleted_by',
|
||||
|
||||
64
app/Models/Tenants/IncomeTaxBracket.php
Normal file
64
app/Models/Tenants/IncomeTaxBracket.php
Normal file
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Tenants;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class IncomeTaxBracket extends Model
|
||||
{
|
||||
protected $table = 'income_tax_brackets';
|
||||
|
||||
protected $fillable = [
|
||||
'tax_year',
|
||||
'salary_from',
|
||||
'salary_to',
|
||||
'family_count',
|
||||
'tax_amount',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'tax_year' => 'integer',
|
||||
'salary_from' => 'integer',
|
||||
'salary_to' => 'integer',
|
||||
'family_count' => 'integer',
|
||||
'tax_amount' => 'integer',
|
||||
];
|
||||
|
||||
public function scopeForYear(Builder $query, int $year): Builder
|
||||
{
|
||||
return $query->where('tax_year', $year);
|
||||
}
|
||||
|
||||
public function scopeForSalaryRange(Builder $query, int $salaryThousand): Builder
|
||||
{
|
||||
return $query->where('salary_from', '<=', $salaryThousand)
|
||||
->where(function ($q) use ($salaryThousand) {
|
||||
$q->where('salary_to', '>', $salaryThousand)
|
||||
->orWhere(function ($q2) use ($salaryThousand) {
|
||||
$q2->whereColumn('salary_from', 'salary_to')
|
||||
->where('salary_from', $salaryThousand);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public function scopeForFamilyCount(Builder $query, int $count): Builder
|
||||
{
|
||||
return $query->where('family_count', $count);
|
||||
}
|
||||
|
||||
/**
|
||||
* 간이세액표에서 세액 조회
|
||||
*/
|
||||
public static function lookupTax(int $year, int $salaryThousand, int $familyCount): int
|
||||
{
|
||||
$familyCount = max(1, min(11, $familyCount));
|
||||
|
||||
$bracket = static::forYear($year)
|
||||
->forSalaryRange($salaryThousand)
|
||||
->forFamilyCount($familyCount)
|
||||
->first();
|
||||
|
||||
return $bracket ? $bracket->tax_amount : 0;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user