Compare commits
84 Commits
3ac64d5b76
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
926a7c7da6 | ||
|
|
877d15420a | ||
|
|
63b174811c | ||
|
|
95bae11042 | ||
|
|
adc07b7343 | ||
|
|
c942788119 | ||
|
|
2e284f6393 | ||
|
|
e12fc461a7 | ||
|
|
1eb8d2cb01 | ||
|
|
39844a3ba0 | ||
|
|
45c30aa2aa | ||
|
|
85d5b98966 | ||
| aeffd5be61 | |||
| d7c096b615 | |||
| 54686cfc8a | |||
|
|
a36b7a2514 | ||
| e241c6a681 | |||
| 73c8f78788 | |||
| 597aecb5e8 | |||
| ef591074c7 | |||
| 8b7d932f00 | |||
| 42d818596d | |||
|
|
0d9a840358 | ||
|
|
2c4f5ee91d | ||
|
|
08582261db | ||
| a50d69b243 | |||
| 37bb691838 | |||
|
|
88d9192618 | ||
|
|
cefad468b9 | ||
|
|
9ad76ceb82 | ||
|
|
76e098337f | ||
| 57d8b97dde | |||
| f5b60aab38 | |||
| f3849808d5 | |||
|
|
2d32faa9b5 | ||
|
|
723b5a8e1a | ||
|
|
8c301b54e3 | ||
|
|
19c524d692 | ||
|
|
964ee40e8d | ||
|
|
f401e17447 | ||
|
|
069d0206a0 | ||
| 8c16993746 | |||
| 3a889b33ef | |||
| 073ad11ecd | |||
| 479059747b | |||
| 12373edf8c | |||
| 3bae303447 | |||
| b55cbc2ec4 | |||
| f0b1b5e33a | |||
|
|
918ae0ebc1 | ||
|
|
2c9e5ae2da | ||
|
|
eeda6d980e | ||
|
|
82621a6045 | ||
|
|
18a6f3e7aa | ||
|
|
5828261dce | ||
|
|
0ab3d5ab88 | ||
|
|
0be88f95ca | ||
|
|
3fd412f89d | ||
|
|
6f48b86206 | ||
|
|
f0464d4f8c | ||
| bbaeefb6b5 | |||
| 079f4b0ffb | |||
| e061faadc2 | |||
| 30c2484440 | |||
| 334e39d2de | |||
| e372b9543b | |||
|
|
4e192d1c00 | ||
|
|
6d1925fcd1 | ||
| 22f72f1bbc | |||
|
|
6f0ad1cf2d | ||
|
|
d8f2361c88 | ||
| 74e3c21ee0 | |||
| 45a207d4a8 | |||
| 3fc5f511bc | |||
|
|
ee9f4d0b8f | ||
|
|
ca259ccb18 | ||
|
|
3929c5fd1e | ||
|
|
56c60ec3df | ||
|
|
60c4256bd0 | ||
|
|
1861f4daf2 | ||
|
|
c62e59ad17 | ||
|
|
e6f13e3870 | ||
|
|
1d5d161e05 | ||
|
|
0044779eb4 |
@@ -114,3 +114,12 @@ symbol_info_budget:
|
|||||||
# Note: the backend is fixed at startup. If a project with a different backend
|
# Note: the backend is fixed at startup. If a project with a different backend
|
||||||
# is activated post-init, an error will be returned.
|
# is activated post-init, an error will be returned.
|
||||||
language_backend:
|
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 {
|
pipeline {
|
||||||
agent any
|
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 {
|
options {
|
||||||
disableConcurrentBuilds()
|
disableConcurrentBuilds()
|
||||||
}
|
}
|
||||||
@@ -8,10 +14,73 @@ pipeline {
|
|||||||
environment {
|
environment {
|
||||||
DEPLOY_USER = 'hskwon'
|
DEPLOY_USER = 'hskwon'
|
||||||
RELEASE_ID = new Date().format('yyyyMMdd_HHmmss')
|
RELEASE_ID = new Date().format('yyyyMMdd_HHmmss')
|
||||||
|
PROD_SERVER = '211.117.60.189'
|
||||||
}
|
}
|
||||||
|
|
||||||
stages {
|
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') {
|
stage('Checkout') {
|
||||||
|
when { expression { params.ACTION == 'deploy' } }
|
||||||
steps {
|
steps {
|
||||||
checkout scm
|
checkout scm
|
||||||
script {
|
script {
|
||||||
@@ -24,17 +93,22 @@ pipeline {
|
|||||||
|
|
||||||
// ── main → 운영서버 Stage 배포 ──
|
// ── main → 운영서버 Stage 배포 ──
|
||||||
stage('Deploy Stage') {
|
stage('Deploy Stage') {
|
||||||
when { branch 'main' }
|
when {
|
||||||
|
allOf {
|
||||||
|
branch 'main'
|
||||||
|
expression { params.ACTION == 'deploy' }
|
||||||
|
}
|
||||||
|
}
|
||||||
steps {
|
steps {
|
||||||
sshagent(credentials: ['deploy-ssh-key']) {
|
sshagent(credentials: ['deploy-ssh-key']) {
|
||||||
sh """
|
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 \
|
rsync -az --delete \
|
||||||
--exclude='.git' --exclude='.env' \
|
--exclude='.git' --exclude='.env' \
|
||||||
--exclude='storage/app' --exclude='storage/logs' \
|
--exclude='storage/app' --exclude='storage/logs' \
|
||||||
--exclude='storage/framework/sessions' --exclude='storage/framework/cache' \
|
--exclude='storage/framework/sessions' --exclude='storage/framework/cache' \
|
||||||
. ${DEPLOY_USER}@211.117.60.189:/home/webservice/api-stage/releases/${RELEASE_ID}/
|
. ${DEPLOY_USER}@${PROD_SERVER}:/home/webservice/api-stage/releases/${RELEASE_ID}/
|
||||||
ssh ${DEPLOY_USER}@211.117.60.189 '
|
ssh ${DEPLOY_USER}@${PROD_SERVER} '
|
||||||
cd /home/webservice/api-stage/releases/${RELEASE_ID} &&
|
cd /home/webservice/api-stage/releases/${RELEASE_ID} &&
|
||||||
mkdir -p bootstrap/cache storage/framework/{views,cache/data,sessions} storage/logs &&
|
mkdir -p bootstrap/cache storage/framework/{views,cache/data,sessions} storage/logs &&
|
||||||
sudo chown -R www-data:webservice storage bootstrap/cache &&
|
sudo chown -R www-data:webservice storage bootstrap/cache &&
|
||||||
@@ -71,17 +145,22 @@ pipeline {
|
|||||||
|
|
||||||
// ── main → 운영서버 Production 배포 ──
|
// ── main → 운영서버 Production 배포 ──
|
||||||
stage('Deploy Production') {
|
stage('Deploy Production') {
|
||||||
when { branch 'main' }
|
when {
|
||||||
|
allOf {
|
||||||
|
branch 'main'
|
||||||
|
expression { params.ACTION == 'deploy' }
|
||||||
|
}
|
||||||
|
}
|
||||||
steps {
|
steps {
|
||||||
sshagent(credentials: ['deploy-ssh-key']) {
|
sshagent(credentials: ['deploy-ssh-key']) {
|
||||||
sh """
|
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 \
|
rsync -az --delete \
|
||||||
--exclude='.git' --exclude='.env' \
|
--exclude='.git' --exclude='.env' \
|
||||||
--exclude='storage/app' --exclude='storage/logs' \
|
--exclude='storage/app' --exclude='storage/logs' \
|
||||||
--exclude='storage/framework/sessions' --exclude='storage/framework/cache' \
|
--exclude='storage/framework/sessions' --exclude='storage/framework/cache' \
|
||||||
. ${DEPLOY_USER}@211.117.60.189:/home/webservice/api/releases/${RELEASE_ID}/
|
. ${DEPLOY_USER}@${PROD_SERVER}:/home/webservice/api/releases/${RELEASE_ID}/
|
||||||
ssh ${DEPLOY_USER}@211.117.60.189 '
|
ssh ${DEPLOY_USER}@${PROD_SERVER} '
|
||||||
cd /home/webservice/api/releases/${RELEASE_ID} &&
|
cd /home/webservice/api/releases/${RELEASE_ID} &&
|
||||||
mkdir -p bootstrap/cache storage/framework/{views,cache/data,sessions} storage/logs &&
|
mkdir -p bootstrap/cache storage/framework/{views,cache/data,sessions} storage/logs &&
|
||||||
sudo chown -R www-data:webservice storage bootstrap/cache &&
|
sudo chown -R www-data:webservice storage bootstrap/cache &&
|
||||||
@@ -109,23 +188,32 @@ pipeline {
|
|||||||
|
|
||||||
post {
|
post {
|
||||||
success {
|
success {
|
||||||
slackSend channel: '#deploy_api', color: 'good', tokenCredentialId: 'slack-token',
|
script {
|
||||||
message: "✅ *api* 배포 성공 (`${env.BRANCH_NAME}`)\n${env.GIT_COMMIT_MSG}\n<${env.BUILD_URL}|빌드 #${env.BUILD_NUMBER}>"
|
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 {
|
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 {
|
script {
|
||||||
if (env.BRANCH_NAME == 'main') {
|
if (params.ACTION == 'deploy') {
|
||||||
sshagent(credentials: ['deploy-ssh-key']) {
|
slackSend channel: '#deploy_api', color: 'danger', tokenCredentialId: 'slack-token',
|
||||||
sh """
|
message: "❌ *api* 배포 실패 (`${env.BRANCH_NAME}`)\n${env.GIT_COMMIT_MSG}\n<${env.BUILD_URL}|빌드 #${env.BUILD_NUMBER}>"
|
||||||
ssh ${DEPLOY_USER}@211.117.60.189 '
|
if (env.BRANCH_NAME == 'main') {
|
||||||
PREV=\$(ls -1dt /home/webservice/api/releases/*/ | sed -n "2p" | xargs basename 2>/dev/null) &&
|
sshagent(credentials: ['deploy-ssh-key']) {
|
||||||
[ -n "\$PREV" ] && ln -sfn /home/webservice/api/releases/\$PREV /home/webservice/api/current &&
|
sh """
|
||||||
sudo systemctl reload php8.4-fpm
|
ssh ${DEPLOY_USER}@${PROD_SERVER} '
|
||||||
' || true
|
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-06 21:25:05
|
> **자동 생성**: 2026-03-12 13:58:25
|
||||||
> **소스**: Eloquent 모델 관계 분석
|
> **소스**: Eloquent 모델 관계 분석
|
||||||
|
|
||||||
## 📊 모델별 관계 현황
|
## 📊 모델별 관계 현황
|
||||||
@@ -26,6 +26,68 @@ ### bad_debt_memos
|
|||||||
- **badDebt()**: belongsTo → `bad_debts`
|
- **badDebt()**: belongsTo → `bad_debts`
|
||||||
- **creator()**: belongsTo → `users`
|
- **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
|
### biddings
|
||||||
**모델**: `App\Models\Bidding\Bidding`
|
**모델**: `App\Models\Bidding\Bidding`
|
||||||
|
|
||||||
@@ -309,6 +371,43 @@ ### esign_signers
|
|||||||
- **contract()**: belongsTo → `esign_contracts`
|
- **contract()**: belongsTo → `esign_contracts`
|
||||||
- **signFields()**: hasMany → `esign_sign_fields`
|
- **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
|
### estimates
|
||||||
**모델**: `App\Models\Estimate\Estimate`
|
**모델**: `App\Models\Estimate\Estimate`
|
||||||
|
|
||||||
@@ -734,6 +833,36 @@ ### push_notification_settings
|
|||||||
**모델**: `App\Models\PushNotificationSetting`
|
**모델**: `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
|
### inspections
|
||||||
**모델**: `App\Models\Qualitys\Inspection`
|
**모델**: `App\Models\Qualitys\Inspection`
|
||||||
|
|
||||||
@@ -838,6 +967,11 @@ ### quote_revisions
|
|||||||
- **quote()**: belongsTo → `quotes`
|
- **quote()**: belongsTo → `quotes`
|
||||||
- **reviser()**: belongsTo → `users`
|
- **reviser()**: belongsTo → `users`
|
||||||
|
|
||||||
|
### account_codes
|
||||||
|
**모델**: `App\Models\Tenants\AccountCode`
|
||||||
|
|
||||||
|
- **children()**: hasMany → `account_codes`
|
||||||
|
|
||||||
### ai_reports
|
### ai_reports
|
||||||
**모델**: `App\Models\Tenants\AiReport`
|
**모델**: `App\Models\Tenants\AiReport`
|
||||||
|
|
||||||
@@ -857,14 +991,24 @@ ### approvals
|
|||||||
**모델**: `App\Models\Tenants\Approval`
|
**모델**: `App\Models\Tenants\Approval`
|
||||||
|
|
||||||
- **form()**: belongsTo → `approval_forms`
|
- **form()**: belongsTo → `approval_forms`
|
||||||
|
- **line()**: belongsTo → `approval_lines`
|
||||||
- **drafter()**: belongsTo → `users`
|
- **drafter()**: belongsTo → `users`
|
||||||
|
- **department()**: belongsTo → `departments`
|
||||||
|
- **parentDocument()**: belongsTo → `approvals`
|
||||||
- **creator()**: belongsTo → `users`
|
- **creator()**: belongsTo → `users`
|
||||||
- **updater()**: belongsTo → `users`
|
- **updater()**: belongsTo → `users`
|
||||||
|
- **childDocuments()**: hasMany → `approvals`
|
||||||
- **steps()**: hasMany → `approval_steps`
|
- **steps()**: hasMany → `approval_steps`
|
||||||
- **approverSteps()**: hasMany → `approval_steps`
|
- **approverSteps()**: hasMany → `approval_steps`
|
||||||
- **referenceSteps()**: hasMany → `approval_steps`
|
- **referenceSteps()**: hasMany → `approval_steps`
|
||||||
- **linkable()**: morphTo → `(Polymorphic)`
|
- **linkable()**: morphTo → `(Polymorphic)`
|
||||||
|
|
||||||
|
### approval_delegations
|
||||||
|
**모델**: `App\Models\Tenants\ApprovalDelegation`
|
||||||
|
|
||||||
|
- **delegator()**: belongsTo → `users`
|
||||||
|
- **delegate()**: belongsTo → `users`
|
||||||
|
|
||||||
### approval_forms
|
### approval_forms
|
||||||
**모델**: `App\Models\Tenants\ApprovalForm`
|
**모델**: `App\Models\Tenants\ApprovalForm`
|
||||||
|
|
||||||
@@ -883,6 +1027,7 @@ ### approval_steps
|
|||||||
|
|
||||||
- **approval()**: belongsTo → `approvals`
|
- **approval()**: belongsTo → `approvals`
|
||||||
- **approver()**: belongsTo → `users`
|
- **approver()**: belongsTo → `users`
|
||||||
|
- **actedBy()**: belongsTo → `users`
|
||||||
|
|
||||||
### attendances
|
### attendances
|
||||||
**모델**: `App\Models\Tenants\Attendance`
|
**모델**: `App\Models\Tenants\Attendance`
|
||||||
@@ -1004,6 +1149,11 @@ ### loans
|
|||||||
- **creator()**: belongsTo → `users`
|
- **creator()**: belongsTo → `users`
|
||||||
- **updater()**: belongsTo → `users`
|
- **updater()**: belongsTo → `users`
|
||||||
|
|
||||||
|
### mail_logs
|
||||||
|
**모델**: `App\Models\Tenants\MailLog`
|
||||||
|
|
||||||
|
- **tenant()**: belongsTo → `tenants`
|
||||||
|
|
||||||
### payments
|
### payments
|
||||||
**모델**: `App\Models\Tenants\Payment`
|
**모델**: `App\Models\Tenants\Payment`
|
||||||
|
|
||||||
@@ -1046,6 +1196,7 @@ ### receivings
|
|||||||
**모델**: `App\Models\Tenants\Receiving`
|
**모델**: `App\Models\Tenants\Receiving`
|
||||||
|
|
||||||
- **item()**: belongsTo → `items`
|
- **item()**: belongsTo → `items`
|
||||||
|
- **certificateFile()**: belongsTo → `files`
|
||||||
- **creator()**: belongsTo → `users`
|
- **creator()**: belongsTo → `users`
|
||||||
|
|
||||||
### salarys
|
### salarys
|
||||||
@@ -1167,6 +1318,11 @@ ### tenant_field_settings
|
|||||||
- **fieldDef()**: belongsTo → `setting_field_defs`
|
- **fieldDef()**: belongsTo → `setting_field_defs`
|
||||||
- **optionGroup()**: belongsTo → `tenant_option_groups`
|
- **optionGroup()**: belongsTo → `tenant_option_groups`
|
||||||
|
|
||||||
|
### tenant_mail_configs
|
||||||
|
**모델**: `App\Models\Tenants\TenantMailConfig`
|
||||||
|
|
||||||
|
- **tenant()**: belongsTo → `tenants`
|
||||||
|
|
||||||
### tenant_option_groups
|
### tenant_option_groups
|
||||||
**모델**: `App\Models\Tenants\TenantOptionGroup`
|
**모델**: `App\Models\Tenants\TenantOptionGroup`
|
||||||
|
|
||||||
|
|||||||
92
app/Console/Commands/CheckDemoExpiredCommand.php
Normal file
92
app/Console/Commands/CheckDemoExpiredCommand.php
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use App\Models\Tenants\Tenant;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 데모 테넌트 만료 체크 및 알림 커맨드
|
||||||
|
*
|
||||||
|
* - 만료 임박 (7일 이내): 파트너에게 알림 로그
|
||||||
|
* - 만료된 테넌트: 비활성 상태로 전환
|
||||||
|
*
|
||||||
|
* 기존 코드 영향 없음: DEMO_TRIAL 테넌트만 대상
|
||||||
|
*
|
||||||
|
* @see docs/features/sales/demo-tenant-policy.md
|
||||||
|
*/
|
||||||
|
class CheckDemoExpiredCommand extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'demo:check-expired
|
||||||
|
{--dry-run : 실제 변경 없이 대상만 표시}';
|
||||||
|
|
||||||
|
protected $description = '데모 체험 테넌트 만료 체크 및 비활성 처리';
|
||||||
|
|
||||||
|
public function handle(): int
|
||||||
|
{
|
||||||
|
// 1. 만료 임박 테넌트 (7일 이내)
|
||||||
|
$expiringSoon = Tenant::withoutGlobalScopes()
|
||||||
|
->where('tenant_type', Tenant::TYPE_DEMO_TRIAL)
|
||||||
|
->where('tenant_st_code', '!=', 'expired')
|
||||||
|
->whereNotNull('demo_expires_at')
|
||||||
|
->where('demo_expires_at', '>', now())
|
||||||
|
->where('demo_expires_at', '<=', now()->addDays(7))
|
||||||
|
->get();
|
||||||
|
|
||||||
|
if ($expiringSoon->isNotEmpty()) {
|
||||||
|
$this->info("만료 임박 테넌트: {$expiringSoon->count()}건");
|
||||||
|
foreach ($expiringSoon as $tenant) {
|
||||||
|
$daysLeft = (int) now()->diffInDays($tenant->demo_expires_at, false);
|
||||||
|
$this->line(" - [{$tenant->id}] {$tenant->company_name} (D-{$daysLeft})");
|
||||||
|
|
||||||
|
Log::info('데모 체험 만료 임박', [
|
||||||
|
'tenant_id' => $tenant->id,
|
||||||
|
'company_name' => $tenant->company_name,
|
||||||
|
'expires_at' => $tenant->demo_expires_at->toDateString(),
|
||||||
|
'days_left' => $daysLeft,
|
||||||
|
'partner_id' => $tenant->demo_source_partner_id,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 이미 만료된 테넌트 → 상태 변경
|
||||||
|
$expired = Tenant::withoutGlobalScopes()
|
||||||
|
->where('tenant_type', Tenant::TYPE_DEMO_TRIAL)
|
||||||
|
->where('tenant_st_code', '!=', 'expired')
|
||||||
|
->whereNotNull('demo_expires_at')
|
||||||
|
->where('demo_expires_at', '<', now())
|
||||||
|
->get();
|
||||||
|
|
||||||
|
if ($expired->isEmpty()) {
|
||||||
|
$this->info('만료 처리 대상 없음');
|
||||||
|
|
||||||
|
return self::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->info("만료 처리 대상: {$expired->count()}건");
|
||||||
|
|
||||||
|
foreach ($expired as $tenant) {
|
||||||
|
$this->line(" - [{$tenant->id}] {$tenant->company_name} (만료: {$tenant->demo_expires_at->toDateString()})");
|
||||||
|
|
||||||
|
if (! $this->option('dry-run')) {
|
||||||
|
$tenant->forceFill(['tenant_st_code' => 'expired']);
|
||||||
|
$tenant->save();
|
||||||
|
|
||||||
|
Log::info('데모 체험 만료 처리', [
|
||||||
|
'tenant_id' => $tenant->id,
|
||||||
|
'company_name' => $tenant->company_name,
|
||||||
|
'partner_id' => $tenant->demo_source_partner_id,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->option('dry-run')) {
|
||||||
|
$this->warn('(dry-run 모드 — 실제 변경 없음)');
|
||||||
|
} else {
|
||||||
|
$this->info(" {$expired->count()}건 만료 처리 완료");
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::SUCCESS;
|
||||||
|
}
|
||||||
|
}
|
||||||
103
app/Console/Commands/CheckDemoInactiveCommand.php
Normal file
103
app/Console/Commands/CheckDemoInactiveCommand.php
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use App\Models\Tenants\Tenant;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 데모 테넌트 비활성 알림 커맨드
|
||||||
|
*
|
||||||
|
* - 7일 이상 활동 없는 데모 테넌트 탐지
|
||||||
|
* - 파트너에게 후속 조치 알림 로그
|
||||||
|
*
|
||||||
|
* 기존 코드 영향 없음: DEMO 테넌트만 대상
|
||||||
|
*
|
||||||
|
* @see docs/features/sales/demo-tenant-policy.md
|
||||||
|
*/
|
||||||
|
class CheckDemoInactiveCommand extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'demo:check-inactive
|
||||||
|
{--days=7 : 비활성 기준 일수}';
|
||||||
|
|
||||||
|
protected $description = '데모 테넌트 비활성 알림 (활동 없는 테넌트 탐지)';
|
||||||
|
|
||||||
|
public function handle(): int
|
||||||
|
{
|
||||||
|
$thresholdDays = (int) $this->option('days');
|
||||||
|
|
||||||
|
$demos = Tenant::withoutGlobalScopes()
|
||||||
|
->whereIn('tenant_type', Tenant::DEMO_TYPES)
|
||||||
|
->where('tenant_st_code', '!=', 'expired')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
if ($demos->isEmpty()) {
|
||||||
|
$this->info('활성 데모 테넌트 없음');
|
||||||
|
|
||||||
|
return self::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
$inactiveCount = 0;
|
||||||
|
|
||||||
|
foreach ($demos as $tenant) {
|
||||||
|
$lastActivity = $this->getLastActivity($tenant->id);
|
||||||
|
|
||||||
|
if (! $lastActivity) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$daysSince = (int) now()->diffInDays($lastActivity);
|
||||||
|
|
||||||
|
if ($daysSince < $thresholdDays) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$inactiveCount++;
|
||||||
|
$this->line(" - [{$tenant->id}] {$tenant->company_name} ({$daysSince}일 비활성)");
|
||||||
|
|
||||||
|
Log::warning('데모 테넌트 비활성 알림', [
|
||||||
|
'tenant_id' => $tenant->id,
|
||||||
|
'company_name' => $tenant->company_name,
|
||||||
|
'tenant_type' => $tenant->tenant_type,
|
||||||
|
'days_inactive' => $daysSince,
|
||||||
|
'last_activity' => $lastActivity->toDateString(),
|
||||||
|
'partner_id' => $tenant->demo_source_partner_id,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($inactiveCount === 0) {
|
||||||
|
$this->info("비활성 테넌트 없음 (기준: {$thresholdDays}일)");
|
||||||
|
} else {
|
||||||
|
$this->info("비활성 테넌트: {$inactiveCount}건 (기준: {$thresholdDays}일)");
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getLastActivity(int $tenantId): ?\Carbon\Carbon
|
||||||
|
{
|
||||||
|
$tables = ['orders', 'quotes', 'items', 'clients'];
|
||||||
|
$latest = null;
|
||||||
|
|
||||||
|
foreach ($tables as $table) {
|
||||||
|
if (! \Schema::hasTable($table) || ! \Schema::hasColumn($table, 'tenant_id')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$date = DB::table($table)
|
||||||
|
->where('tenant_id', $tenantId)
|
||||||
|
->max('updated_at');
|
||||||
|
|
||||||
|
if ($date) {
|
||||||
|
$parsed = \Carbon\Carbon::parse($date);
|
||||||
|
if (! $latest || $parsed->gt($latest)) {
|
||||||
|
$latest = $parsed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $latest;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -40,8 +40,8 @@ public function handle(): int
|
|||||||
foreach ($files as $file) {
|
foreach ($files as $file) {
|
||||||
try {
|
try {
|
||||||
// Delete physical file
|
// Delete physical file
|
||||||
if (Storage::disk('tenant')->exists($file->file_path)) {
|
if (Storage::disk('r2')->exists($file->file_path)) {
|
||||||
Storage::disk('tenant')->delete($file->file_path);
|
Storage::disk('r2')->delete($file->file_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Force delete from DB
|
// Force delete from DB
|
||||||
|
|||||||
@@ -60,8 +60,8 @@ private function permanentDelete(File $file): void
|
|||||||
{
|
{
|
||||||
DB::transaction(function () use ($file) {
|
DB::transaction(function () use ($file) {
|
||||||
// Delete physical file
|
// Delete physical file
|
||||||
if (Storage::disk('tenant')->exists($file->file_path)) {
|
if (Storage::disk('r2')->exists($file->file_path)) {
|
||||||
Storage::disk('tenant')->delete($file->file_path);
|
Storage::disk('r2')->delete($file->file_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update tenant storage usage
|
// Update tenant storage usage
|
||||||
|
|||||||
174
app/Console/Commands/ResetDemoShowcaseCommand.php
Normal file
174
app/Console/Commands/ResetDemoShowcaseCommand.php
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use App\Models\Tenants\Tenant;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 데모 쇼케이스 테넌트 데이터 리셋 커맨드
|
||||||
|
*
|
||||||
|
* 매일 자정에 쇼케이스 테넌트의 비즈니스 데이터를 삭제하고
|
||||||
|
* 샘플 데이터를 다시 시드한다.
|
||||||
|
*
|
||||||
|
* 기존 코드 영향 없음: DEMO_SHOWCASE 테넌트만 대상
|
||||||
|
*
|
||||||
|
* @see docs/features/sales/demo-tenant-policy.md
|
||||||
|
*/
|
||||||
|
class ResetDemoShowcaseCommand extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'demo:reset-showcase
|
||||||
|
{--seed : 리셋 후 샘플 데이터 시드}
|
||||||
|
{--dry-run : 실제 삭제 없이 대상만 표시}';
|
||||||
|
|
||||||
|
protected $description = '데모 쇼케이스 테넌트의 비즈니스 데이터를 리셋합니다';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 리셋 대상 테이블 목록 (tenant_id 기반)
|
||||||
|
* 순서 중요: FK 의존성 역순으로 삭제
|
||||||
|
*/
|
||||||
|
private const RESET_TABLES = [
|
||||||
|
// 영업/주문
|
||||||
|
'order_item_components',
|
||||||
|
'order_items',
|
||||||
|
'order_histories',
|
||||||
|
'orders',
|
||||||
|
'quotes',
|
||||||
|
|
||||||
|
// 생산
|
||||||
|
'production_results',
|
||||||
|
'production_plans',
|
||||||
|
|
||||||
|
// 자재/재고
|
||||||
|
'material_inspection_items',
|
||||||
|
'material_inspections',
|
||||||
|
'material_receipts',
|
||||||
|
'lot_sales',
|
||||||
|
'lots',
|
||||||
|
|
||||||
|
// 마스터
|
||||||
|
'price_histories',
|
||||||
|
'product_components',
|
||||||
|
'items',
|
||||||
|
'clients',
|
||||||
|
|
||||||
|
// 파일 (데모 데이터 관련)
|
||||||
|
// files는 morphable이므로 별도 처리 필요
|
||||||
|
|
||||||
|
// 조직
|
||||||
|
'departments',
|
||||||
|
|
||||||
|
// 감사 로그 (데모 데이터)
|
||||||
|
'audit_logs',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function handle(): int
|
||||||
|
{
|
||||||
|
$showcases = Tenant::withoutGlobalScopes()
|
||||||
|
->where('tenant_type', Tenant::TYPE_DEMO_SHOWCASE)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
if ($showcases->isEmpty()) {
|
||||||
|
$this->info('데모 쇼케이스 테넌트가 없습니다.');
|
||||||
|
|
||||||
|
return self::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($showcases as $tenant) {
|
||||||
|
$this->info("리셋 대상: [{$tenant->id}] {$tenant->company_name}");
|
||||||
|
|
||||||
|
if ($this->option('dry-run')) {
|
||||||
|
$this->showStats($tenant);
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->resetTenantData($tenant);
|
||||||
|
|
||||||
|
if ($this->option('seed')) {
|
||||||
|
$this->seedSampleData($tenant);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function showStats(Tenant $tenant): void
|
||||||
|
{
|
||||||
|
foreach (self::RESET_TABLES as $table) {
|
||||||
|
if (! \Schema::hasTable($table)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! \Schema::hasColumn($table, 'tenant_id')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$count = DB::table($table)->where('tenant_id', $tenant->id)->count();
|
||||||
|
if ($count > 0) {
|
||||||
|
$this->line(" - {$table}: {$count}건");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resetTenantData(Tenant $tenant): void
|
||||||
|
{
|
||||||
|
$totalDeleted = 0;
|
||||||
|
|
||||||
|
DB::beginTransaction();
|
||||||
|
try {
|
||||||
|
foreach (self::RESET_TABLES as $table) {
|
||||||
|
if (! \Schema::hasTable($table)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! \Schema::hasColumn($table, 'tenant_id')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$deleted = DB::table($table)->where('tenant_id', $tenant->id)->delete();
|
||||||
|
if ($deleted > 0) {
|
||||||
|
$this->line(" 삭제: {$table} → {$deleted}건");
|
||||||
|
$totalDeleted += $deleted;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DB::commit();
|
||||||
|
$this->info(" 총 {$totalDeleted}건 삭제 완료");
|
||||||
|
|
||||||
|
Log::info('데모 쇼케이스 리셋 완료', [
|
||||||
|
'tenant_id' => $tenant->id,
|
||||||
|
'deleted_count' => $totalDeleted,
|
||||||
|
]);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
DB::rollBack();
|
||||||
|
$this->error(" 리셋 실패: {$e->getMessage()}");
|
||||||
|
Log::error('데모 쇼케이스 리셋 실패', [
|
||||||
|
'tenant_id' => $tenant->id,
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function seedSampleData(Tenant $tenant): void
|
||||||
|
{
|
||||||
|
$preset = $tenant->getDemoPreset() ?? 'manufacturing';
|
||||||
|
$this->info(" 샘플 데이터 시드: {$preset}");
|
||||||
|
|
||||||
|
try {
|
||||||
|
$seeder = new \Database\Seeders\Demo\ManufacturingPresetSeeder;
|
||||||
|
$seeder->run($tenant->id);
|
||||||
|
$this->info(' 샘플 데이터 시드 완료');
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->error(" 시드 실패: {$e->getMessage()}");
|
||||||
|
Log::error('데모 샘플 데이터 시드 실패', [
|
||||||
|
'tenant_id' => $tenant->id,
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
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\Helpers\ApiResponse;
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Http\Requests\V1\AccountSubject\StoreAccountSubjectRequest;
|
use App\Http\Requests\V1\AccountSubject\StoreAccountSubjectRequest;
|
||||||
|
use App\Http\Requests\V1\AccountSubject\UpdateAccountSubjectRequest;
|
||||||
use App\Services\AccountCodeService;
|
use App\Services\AccountCodeService;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
@@ -19,7 +20,10 @@ public function __construct(
|
|||||||
*/
|
*/
|
||||||
public function index(Request $request)
|
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);
|
$subjects = $this->service->index($params);
|
||||||
|
|
||||||
@@ -36,6 +40,16 @@ public function store(StoreAccountSubjectRequest $request)
|
|||||||
return ApiResponse::success($subject, __('message.created'), [], 201);
|
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'));
|
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\Helpers\ApiResponse;
|
||||||
use App\Http\Controllers\Controller;
|
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\InboxIndexRequest;
|
||||||
use App\Http\Requests\Approval\IndexRequest;
|
use App\Http\Requests\Approval\IndexRequest;
|
||||||
|
use App\Http\Requests\Approval\PreDecideRequest;
|
||||||
use App\Http\Requests\Approval\ReferenceIndexRequest;
|
use App\Http\Requests\Approval\ReferenceIndexRequest;
|
||||||
use App\Http\Requests\Approval\RejectRequest;
|
use App\Http\Requests\Approval\RejectRequest;
|
||||||
use App\Http\Requests\Approval\StoreRequest;
|
use App\Http\Requests\Approval\StoreRequest;
|
||||||
@@ -133,10 +139,10 @@ public function submit(int $id, SubmitRequest $request): JsonResponse
|
|||||||
* 결재 승인
|
* 결재 승인
|
||||||
* POST /v1/approvals/{id}/approve
|
* 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 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'));
|
}, __('message.approval.approved'));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,11 +161,99 @@ public function reject(int $id, RejectRequest $request): JsonResponse
|
|||||||
* 결재 회수 (기안자만)
|
* 결재 회수 (기안자만)
|
||||||
* POST /v1/approvals/{id}/cancel
|
* 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 ApiResponse::handle(function () use ($id) {
|
||||||
return $this->service->cancel($id);
|
return $this->service->releaseHold($id);
|
||||||
}, __('message.approval.cancelled'));
|
}, __('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);
|
return $this->service->markUnread($id);
|
||||||
}, __('message.approval.marked_unread'));
|
}, __('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,
|
'barobill_id' => $setting->barobill_id,
|
||||||
'biz_no' => $setting->corp_num,
|
'biz_no' => $setting->corp_num,
|
||||||
'status' => $setting->isVerified() ? 'active' : 'inactive',
|
'status' => $setting->isVerified() ? 'active' : 'inactive',
|
||||||
'server_mode' => config('services.barobill.test_mode', true) ? 'test' : 'production',
|
'server_mode' => $this->barobillService->isTestMode() ? 'test' : 'production',
|
||||||
] : null,
|
] : null,
|
||||||
];
|
];
|
||||||
}, __('message.fetched'));
|
}, __('message.fetched'));
|
||||||
@@ -86,17 +86,21 @@ public function signup(Request $request)
|
|||||||
}, __('message.saved'));
|
}, __('message.saved'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 바로빌 서비스 URL 조회 (공통)
|
||||||
|
*/
|
||||||
|
private function getServiceUrl(string $path): array
|
||||||
|
{
|
||||||
|
return ['url' => $this->barobillService->getBaseUrl().$path];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 은행 빠른조회 서비스 URL 조회
|
* 은행 빠른조회 서비스 URL 조회
|
||||||
*/
|
*/
|
||||||
public function bankServiceUrl(Request $request)
|
public function bankServiceUrl()
|
||||||
{
|
{
|
||||||
return ApiResponse::handle(function () {
|
return ApiResponse::handle(function () {
|
||||||
$baseUrl = config('services.barobill.test_mode', true)
|
return $this->getServiceUrl('/BANKACCOUNT.asmx');
|
||||||
? 'https://testws.barobill.co.kr'
|
|
||||||
: 'https://ws.barobill.co.kr';
|
|
||||||
|
|
||||||
return ['url' => $baseUrl.'/Bank/BankAccountService'];
|
|
||||||
}, __('message.fetched'));
|
}, __('message.fetched'));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,11 +110,7 @@ public function bankServiceUrl(Request $request)
|
|||||||
public function accountLinkUrl()
|
public function accountLinkUrl()
|
||||||
{
|
{
|
||||||
return ApiResponse::handle(function () {
|
return ApiResponse::handle(function () {
|
||||||
$baseUrl = config('services.barobill.test_mode', true)
|
return $this->getServiceUrl('/BANKACCOUNT.asmx');
|
||||||
? 'https://testws.barobill.co.kr'
|
|
||||||
: 'https://ws.barobill.co.kr';
|
|
||||||
|
|
||||||
return ['url' => $baseUrl.'/Bank/AccountLink'];
|
|
||||||
}, __('message.fetched'));
|
}, __('message.fetched'));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,11 +120,7 @@ public function accountLinkUrl()
|
|||||||
public function cardLinkUrl()
|
public function cardLinkUrl()
|
||||||
{
|
{
|
||||||
return ApiResponse::handle(function () {
|
return ApiResponse::handle(function () {
|
||||||
$baseUrl = config('services.barobill.test_mode', true)
|
return $this->getServiceUrl('/CARD.asmx');
|
||||||
? 'https://testws.barobill.co.kr'
|
|
||||||
: 'https://ws.barobill.co.kr';
|
|
||||||
|
|
||||||
return ['url' => $baseUrl.'/Card/CardLink'];
|
|
||||||
}, __('message.fetched'));
|
}, __('message.fetched'));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,11 +130,7 @@ public function cardLinkUrl()
|
|||||||
public function certificateUrl()
|
public function certificateUrl()
|
||||||
{
|
{
|
||||||
return ApiResponse::handle(function () {
|
return ApiResponse::handle(function () {
|
||||||
$baseUrl = config('services.barobill.test_mode', true)
|
return $this->getServiceUrl('/CORPSTATE.asmx');
|
||||||
? 'https://testws.barobill.co.kr'
|
|
||||||
: 'https://ws.barobill.co.kr';
|
|
||||||
|
|
||||||
return ['url' => $baseUrl.'/Certificate/Register'];
|
|
||||||
}, __('message.fetched'));
|
}, __('message.fetched'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,12 +18,9 @@ public function __construct(
|
|||||||
*/
|
*/
|
||||||
public function show()
|
public function show()
|
||||||
{
|
{
|
||||||
$setting = $this->barobillService->getSetting();
|
return ApiResponse::handle(function () {
|
||||||
|
return $this->barobillService->getSetting();
|
||||||
return ApiResponse::handle(
|
}, __('message.fetched'));
|
||||||
data: $setting,
|
|
||||||
message: __('message.fetched')
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -31,12 +28,9 @@ public function show()
|
|||||||
*/
|
*/
|
||||||
public function save(SaveBarobillSettingRequest $request)
|
public function save(SaveBarobillSettingRequest $request)
|
||||||
{
|
{
|
||||||
$setting = $this->barobillService->saveSetting($request->validated());
|
return ApiResponse::handle(function () use ($request) {
|
||||||
|
return $this->barobillService->saveSetting($request->validated());
|
||||||
return ApiResponse::handle(
|
}, __('message.saved'));
|
||||||
data: $setting,
|
|
||||||
message: __('message.saved')
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -44,11 +38,8 @@ public function save(SaveBarobillSettingRequest $request)
|
|||||||
*/
|
*/
|
||||||
public function testConnection()
|
public function testConnection()
|
||||||
{
|
{
|
||||||
$result = $this->barobillService->testConnection();
|
return ApiResponse::handle(function () {
|
||||||
|
return $this->barobillService->testConnection();
|
||||||
return ApiResponse::handle(
|
}, __('message.barobill.connection_success'));
|
||||||
data: $result,
|
|
||||||
message: __('message.barobill.connection_success')
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,9 @@
|
|||||||
|
|
||||||
use App\Helpers\ApiResponse;
|
use App\Helpers\ApiResponse;
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\Tenants\JournalEntry;
|
||||||
use App\Services\CardTransactionService;
|
use App\Services\CardTransactionService;
|
||||||
|
use App\Services\JournalSyncService;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
@@ -14,7 +16,8 @@
|
|||||||
class CardTransactionController extends Controller
|
class CardTransactionController extends Controller
|
||||||
{
|
{
|
||||||
public function __construct(
|
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);
|
return $this->service->destroy($id);
|
||||||
}, __('message.deleted'));
|
}, __('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'));
|
||||||
|
}
|
||||||
|
}
|
||||||
62
app/Http/Controllers/Api/V1/DemoAnalyticsController.php
Normal file
62
app/Http/Controllers/Api/V1/DemoAnalyticsController.php
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api\V1;
|
||||||
|
|
||||||
|
use App\Helpers\ApiResponse;
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Services\Demo\DemoAnalyticsService;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 데모 테넌트 분석 API 컨트롤러
|
||||||
|
*
|
||||||
|
* 전환율, 파트너 성과, 활동 현황 등 데모 분석 엔드포인트
|
||||||
|
*
|
||||||
|
* 기존 코드 영향 없음: 데모 전용 라우트에서만 사용
|
||||||
|
*
|
||||||
|
* @see docs/features/sales/demo-tenant-policy.md
|
||||||
|
*/
|
||||||
|
class DemoAnalyticsController extends Controller
|
||||||
|
{
|
||||||
|
public function __construct(private DemoAnalyticsService $service) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 대시보드 요약
|
||||||
|
*/
|
||||||
|
public function summary()
|
||||||
|
{
|
||||||
|
return ApiResponse::handle(function () {
|
||||||
|
return $this->service->summary();
|
||||||
|
}, __('message.fetched'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 전환율 퍼널 분석
|
||||||
|
*/
|
||||||
|
public function conversionFunnel(Request $request)
|
||||||
|
{
|
||||||
|
return ApiResponse::handle(function () use ($request) {
|
||||||
|
return $this->service->conversionFunnel($request->all());
|
||||||
|
}, __('message.fetched'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 파트너별 성과 분석
|
||||||
|
*/
|
||||||
|
public function partnerPerformance(Request $request)
|
||||||
|
{
|
||||||
|
return ApiResponse::handle(function () use ($request) {
|
||||||
|
return $this->service->partnerPerformance($request->all());
|
||||||
|
}, __('message.fetched'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 데모 테넌트 활동 현황
|
||||||
|
*/
|
||||||
|
public function activityReport(Request $request)
|
||||||
|
{
|
||||||
|
return ApiResponse::handle(function () use ($request) {
|
||||||
|
return $this->service->activityReport($request->all());
|
||||||
|
}, __('message.fetched'));
|
||||||
|
}
|
||||||
|
}
|
||||||
95
app/Http/Controllers/Api/V1/DemoTenantController.php
Normal file
95
app/Http/Controllers/Api/V1/DemoTenantController.php
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api\V1;
|
||||||
|
|
||||||
|
use App\Helpers\ApiResponse;
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Http\Requests\Demo\DemoTenantStoreRequest;
|
||||||
|
use App\Services\Demo\DemoTenantService;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 데모 테넌트 관리 API 컨트롤러
|
||||||
|
*
|
||||||
|
* 파트너가 고객 체험 테넌트를 생성/관리하는 엔드포인트
|
||||||
|
*
|
||||||
|
* 기존 코드 영향 없음: 데모 전용 라우트에서만 사용
|
||||||
|
*
|
||||||
|
* @see docs/features/sales/demo-tenant-policy.md
|
||||||
|
*/
|
||||||
|
class DemoTenantController extends Controller
|
||||||
|
{
|
||||||
|
public function __construct(private DemoTenantService $service) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 내가 생성한 데모 테넌트 목록
|
||||||
|
*/
|
||||||
|
public function index(Request $request)
|
||||||
|
{
|
||||||
|
return ApiResponse::handle(function () use ($request) {
|
||||||
|
return $this->service->index($request->all());
|
||||||
|
}, __('message.demo_tenant.fetched'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 데모 테넌트 상세 조회
|
||||||
|
*/
|
||||||
|
public function show(int $id)
|
||||||
|
{
|
||||||
|
return ApiResponse::handle(function () use ($id) {
|
||||||
|
return $this->service->show($id);
|
||||||
|
}, __('message.demo_tenant.fetched'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 고객 체험 테넌트 생성 (Tier 3)
|
||||||
|
*/
|
||||||
|
public function store(DemoTenantStoreRequest $request)
|
||||||
|
{
|
||||||
|
return ApiResponse::handle(function () use ($request) {
|
||||||
|
return $this->service->createTrialFromApi($request->validated());
|
||||||
|
}, __('message.demo_tenant.created'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 데모 데이터 리셋
|
||||||
|
*/
|
||||||
|
public function reset(int $id)
|
||||||
|
{
|
||||||
|
return ApiResponse::handle(function () use ($id) {
|
||||||
|
return $this->service->resetFromApi($id);
|
||||||
|
}, __('message.demo_tenant.reset'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 체험 기간 연장
|
||||||
|
*/
|
||||||
|
public function extend(int $id, Request $request)
|
||||||
|
{
|
||||||
|
return ApiResponse::handle(function () use ($id, $request) {
|
||||||
|
$days = (int) $request->input('days', 30);
|
||||||
|
|
||||||
|
return $this->service->extendFromApi($id, $days);
|
||||||
|
}, __('message.demo_tenant.extended'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 데모 → 정식 전환
|
||||||
|
*/
|
||||||
|
public function convert(int $id)
|
||||||
|
{
|
||||||
|
return ApiResponse::handle(function () use ($id) {
|
||||||
|
return $this->service->convertFromApi($id);
|
||||||
|
}, __('message.demo_tenant.converted'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 데모 현황 통계
|
||||||
|
*/
|
||||||
|
public function stats()
|
||||||
|
{
|
||||||
|
return ApiResponse::handle(function () {
|
||||||
|
return $this->service->stats();
|
||||||
|
}, __('message.fetched'));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -83,14 +83,25 @@ public function trash()
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Download file
|
* Download file (attachment)
|
||||||
*/
|
*/
|
||||||
public function download(int $id)
|
public function download(int $id)
|
||||||
{
|
{
|
||||||
$service = new FileStorageService;
|
$service = new FileStorageService;
|
||||||
$file = $service->getFile($id);
|
$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;
|
$filePath = $directory.'/'.$storedName;
|
||||||
|
|
||||||
// 파일 저장 (tenant 디스크)
|
// 파일 저장 (tenant 디스크)
|
||||||
Storage::disk('tenant')->putFileAs($directory, $uploadedFile, $storedName);
|
Storage::disk('r2')->putFileAs($directory, $uploadedFile, $storedName);
|
||||||
|
|
||||||
// file_type 자동 분류 (MIME 타입 기반)
|
// file_type 자동 분류 (MIME 타입 기반)
|
||||||
$mimeType = $uploadedFile->getMimeType();
|
$mimeType = $uploadedFile->getMimeType();
|
||||||
|
|||||||
@@ -4,18 +4,24 @@
|
|||||||
|
|
||||||
use App\Helpers\ApiResponse;
|
use App\Helpers\ApiResponse;
|
||||||
use App\Http\Controllers\Controller;
|
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\CalculatePayrollRequest;
|
||||||
|
use App\Http\Requests\V1\Payroll\CopyFromPreviousPayrollRequest;
|
||||||
use App\Http\Requests\V1\Payroll\PayPayrollRequest;
|
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\StorePayrollRequest;
|
||||||
use App\Http\Requests\V1\Payroll\UpdatePayrollRequest;
|
use App\Http\Requests\V1\Payroll\UpdatePayrollRequest;
|
||||||
use App\Http\Requests\V1\Payroll\UpdatePayrollSettingRequest;
|
use App\Http\Requests\V1\Payroll\UpdatePayrollSettingRequest;
|
||||||
|
use App\Services\ExportService;
|
||||||
use App\Services\PayrollService;
|
use App\Services\PayrollService;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
||||||
|
|
||||||
class PayrollController extends Controller
|
class PayrollController extends Controller
|
||||||
{
|
{
|
||||||
public function __construct(
|
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',
|
'month',
|
||||||
'user_id',
|
'user_id',
|
||||||
'status',
|
'status',
|
||||||
|
'department_id',
|
||||||
'search',
|
'search',
|
||||||
'sort_by',
|
'sort_by',
|
||||||
'sort_dir',
|
'sort_dir',
|
||||||
@@ -103,6 +110,16 @@ public function confirm(int $id)
|
|||||||
return ApiResponse::success($payroll, __('message.payroll.confirmed'));
|
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'));
|
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'));
|
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'));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -124,4 +124,24 @@ public function resultDocument(int $id)
|
|||||||
return $this->service->resultDocument($id);
|
return $this->service->resultDocument($id);
|
||||||
}, __('message.fetched'));
|
}, __('message.fetched'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function uploadFile(Request $request, int $id)
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'file' => ['required', 'file', 'max:51200'], // 50MB
|
||||||
|
]);
|
||||||
|
|
||||||
|
return ApiResponse::handle(function () use ($request, $id) {
|
||||||
|
return $this->service->uploadFile($id, $request->file('file'));
|
||||||
|
}, __('message.created'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function deleteFile(int $id)
|
||||||
|
{
|
||||||
|
return ApiResponse::handle(function () use ($id) {
|
||||||
|
$this->service->deleteFile($id);
|
||||||
|
|
||||||
|
return 'success';
|
||||||
|
}, __('message.deleted'));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,13 +8,15 @@
|
|||||||
use App\Http\Requests\Shipment\ShipmentUpdateRequest;
|
use App\Http\Requests\Shipment\ShipmentUpdateRequest;
|
||||||
use App\Http\Requests\Shipment\ShipmentUpdateStatusRequest;
|
use App\Http\Requests\Shipment\ShipmentUpdateStatusRequest;
|
||||||
use App\Services\ShipmentService;
|
use App\Services\ShipmentService;
|
||||||
|
use App\Services\WorkOrderService;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
class ShipmentController extends Controller
|
class ShipmentController extends Controller
|
||||||
{
|
{
|
||||||
public function __construct(
|
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());
|
$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 옵션 조회
|
* LOT 옵션 조회
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -10,12 +10,17 @@
|
|||||||
use App\Http\Requests\TaxInvoice\TaxInvoiceSummaryRequest;
|
use App\Http\Requests\TaxInvoice\TaxInvoiceSummaryRequest;
|
||||||
use App\Http\Requests\TaxInvoice\UpdateTaxInvoiceRequest;
|
use App\Http\Requests\TaxInvoice\UpdateTaxInvoiceRequest;
|
||||||
use App\Http\Requests\V1\TaxInvoice\BulkIssueRequest;
|
use App\Http\Requests\V1\TaxInvoice\BulkIssueRequest;
|
||||||
|
use App\Models\Tenants\JournalEntry;
|
||||||
|
use App\Services\JournalSyncService;
|
||||||
use App\Services\TaxInvoiceService;
|
use App\Services\TaxInvoiceService;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
class TaxInvoiceController extends Controller
|
class TaxInvoiceController extends Controller
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private TaxInvoiceService $taxInvoiceService
|
private TaxInvoiceService $taxInvoiceService,
|
||||||
|
private JournalSyncService $journalSyncService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -23,12 +28,9 @@ public function __construct(
|
|||||||
*/
|
*/
|
||||||
public function index(TaxInvoiceListRequest $request)
|
public function index(TaxInvoiceListRequest $request)
|
||||||
{
|
{
|
||||||
$taxInvoices = $this->taxInvoiceService->list($request->validated());
|
return ApiResponse::handle(function () use ($request) {
|
||||||
|
return $this->taxInvoiceService->list($request->validated());
|
||||||
return ApiResponse::handle(
|
}, __('message.fetched'));
|
||||||
data: $taxInvoices,
|
|
||||||
message: __('message.fetched')
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -36,12 +38,9 @@ public function index(TaxInvoiceListRequest $request)
|
|||||||
*/
|
*/
|
||||||
public function show(int $id)
|
public function show(int $id)
|
||||||
{
|
{
|
||||||
$taxInvoice = $this->taxInvoiceService->show($id);
|
return ApiResponse::handle(function () use ($id) {
|
||||||
|
return $this->taxInvoiceService->show($id);
|
||||||
return ApiResponse::handle(
|
}, __('message.fetched'));
|
||||||
data: $taxInvoice,
|
|
||||||
message: __('message.fetched')
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -49,13 +48,9 @@ public function show(int $id)
|
|||||||
*/
|
*/
|
||||||
public function store(CreateTaxInvoiceRequest $request)
|
public function store(CreateTaxInvoiceRequest $request)
|
||||||
{
|
{
|
||||||
$taxInvoice = $this->taxInvoiceService->create($request->validated());
|
return ApiResponse::handle(function () use ($request) {
|
||||||
|
return $this->taxInvoiceService->create($request->validated());
|
||||||
return ApiResponse::handle(
|
}, __('message.created'));
|
||||||
data: $taxInvoice,
|
|
||||||
message: __('message.created'),
|
|
||||||
status: 201
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -63,12 +58,9 @@ public function store(CreateTaxInvoiceRequest $request)
|
|||||||
*/
|
*/
|
||||||
public function update(UpdateTaxInvoiceRequest $request, int $id)
|
public function update(UpdateTaxInvoiceRequest $request, int $id)
|
||||||
{
|
{
|
||||||
$taxInvoice = $this->taxInvoiceService->update($id, $request->validated());
|
return ApiResponse::handle(function () use ($request, $id) {
|
||||||
|
return $this->taxInvoiceService->update($id, $request->validated());
|
||||||
return ApiResponse::handle(
|
}, __('message.updated'));
|
||||||
data: $taxInvoice,
|
|
||||||
message: __('message.updated')
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -76,12 +68,11 @@ public function update(UpdateTaxInvoiceRequest $request, int $id)
|
|||||||
*/
|
*/
|
||||||
public function destroy(int $id)
|
public function destroy(int $id)
|
||||||
{
|
{
|
||||||
$this->taxInvoiceService->delete($id);
|
return ApiResponse::handle(function () use ($id) {
|
||||||
|
$this->taxInvoiceService->delete($id);
|
||||||
|
|
||||||
return ApiResponse::handle(
|
return null;
|
||||||
data: null,
|
}, __('message.deleted'));
|
||||||
message: __('message.deleted')
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -89,12 +80,9 @@ public function destroy(int $id)
|
|||||||
*/
|
*/
|
||||||
public function issue(int $id)
|
public function issue(int $id)
|
||||||
{
|
{
|
||||||
$taxInvoice = $this->taxInvoiceService->issue($id);
|
return ApiResponse::handle(function () use ($id) {
|
||||||
|
return $this->taxInvoiceService->issue($id);
|
||||||
return ApiResponse::handle(
|
}, __('message.tax_invoice.issued'));
|
||||||
data: $taxInvoice,
|
|
||||||
message: __('message.tax_invoice.issued')
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -102,12 +90,9 @@ public function issue(int $id)
|
|||||||
*/
|
*/
|
||||||
public function bulkIssue(BulkIssueRequest $request)
|
public function bulkIssue(BulkIssueRequest $request)
|
||||||
{
|
{
|
||||||
$result = $this->taxInvoiceService->bulkIssue($request->getIds());
|
return ApiResponse::handle(function () use ($request) {
|
||||||
|
return $this->taxInvoiceService->bulkIssue($request->getIds());
|
||||||
return ApiResponse::handle(
|
}, __('message.tax_invoice.bulk_issued'));
|
||||||
data: $result,
|
|
||||||
message: __('message.tax_invoice.bulk_issued')
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -115,12 +100,9 @@ public function bulkIssue(BulkIssueRequest $request)
|
|||||||
*/
|
*/
|
||||||
public function cancel(CancelTaxInvoiceRequest $request, int $id)
|
public function cancel(CancelTaxInvoiceRequest $request, int $id)
|
||||||
{
|
{
|
||||||
$taxInvoice = $this->taxInvoiceService->cancel($id, $request->validated()['reason']);
|
return ApiResponse::handle(function () use ($request, $id) {
|
||||||
|
return $this->taxInvoiceService->cancel($id, $request->validated()['reason']);
|
||||||
return ApiResponse::handle(
|
}, __('message.tax_invoice.cancelled'));
|
||||||
data: $taxInvoice,
|
|
||||||
message: __('message.tax_invoice.cancelled')
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -128,12 +110,9 @@ public function cancel(CancelTaxInvoiceRequest $request, int $id)
|
|||||||
*/
|
*/
|
||||||
public function checkStatus(int $id)
|
public function checkStatus(int $id)
|
||||||
{
|
{
|
||||||
$taxInvoice = $this->taxInvoiceService->checkStatus($id);
|
return ApiResponse::handle(function () use ($id) {
|
||||||
|
return $this->taxInvoiceService->checkStatus($id);
|
||||||
return ApiResponse::handle(
|
}, __('message.fetched'));
|
||||||
data: $taxInvoice,
|
|
||||||
message: __('message.fetched')
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -141,11 +120,79 @@ public function checkStatus(int $id)
|
|||||||
*/
|
*/
|
||||||
public function summary(TaxInvoiceSummaryRequest $request)
|
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,
|
// 분개 (Journal Entries)
|
||||||
message: __('message.fetched')
|
// =========================================================================
|
||||||
);
|
|
||||||
|
/**
|
||||||
|
* 세금계산서 분개 조회
|
||||||
|
*/
|
||||||
|
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'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
38
app/Http/Controllers/Api/V1/VehiclePhotoController.php
Normal file
38
app/Http/Controllers/Api/V1/VehiclePhotoController.php
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api\V1;
|
||||||
|
|
||||||
|
use App\Helpers\ApiResponse;
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Http\Requests\Finance\StoreVehiclePhotoRequest;
|
||||||
|
use App\Services\Finance\VehiclePhotoService;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
|
||||||
|
class VehiclePhotoController extends Controller
|
||||||
|
{
|
||||||
|
public function __construct(private readonly VehiclePhotoService $service) {}
|
||||||
|
|
||||||
|
public function index(int $id): JsonResponse
|
||||||
|
{
|
||||||
|
return ApiResponse::handle(
|
||||||
|
fn () => $this->service->index($id),
|
||||||
|
__('message.fetched')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(StoreVehiclePhotoRequest $request, int $id): JsonResponse
|
||||||
|
{
|
||||||
|
return ApiResponse::handle(
|
||||||
|
fn () => $this->service->store($id, $request->file('files')),
|
||||||
|
__('message.created')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy(int $id, int $fileId): JsonResponse
|
||||||
|
{
|
||||||
|
return ApiResponse::handle(
|
||||||
|
fn () => $this->service->destroy($id, $fileId),
|
||||||
|
__('message.deleted')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -61,14 +61,18 @@ public function detail(Request $request): JsonResponse
|
|||||||
: 0.05;
|
: 0.05;
|
||||||
$year = $request->query('year') ? (int) $request->query('year') : null;
|
$year = $request->query('year') ? (int) $request->query('year') : null;
|
||||||
$quarter = $request->query('quarter') ? (int) $request->query('quarter') : 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(
|
return $this->welfareService->getDetail(
|
||||||
$calculationType,
|
$calculationType,
|
||||||
$fixedAmountPerMonth,
|
$fixedAmountPerMonth,
|
||||||
$ratio,
|
$ratio,
|
||||||
$year,
|
$year,
|
||||||
$quarter
|
$quarter,
|
||||||
|
$startDate,
|
||||||
|
$endDate
|
||||||
);
|
);
|
||||||
}, __('message.fetched'));
|
}, __('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(Request $request, int $id): JsonResponse
|
||||||
|
{
|
||||||
|
return ApiResponse::handle(
|
||||||
|
fn () => $this->service->getTemplatesByEquipment($id, $request->input('cycle')),
|
||||||
|
__('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')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\V1\Vehicle;
|
||||||
|
|
||||||
|
use App\Helpers\ApiResponse;
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Services\Vehicle\CorporateVehicleService;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class CorporateVehicleController extends Controller
|
||||||
|
{
|
||||||
|
public function __construct(private readonly CorporateVehicleService $service) {}
|
||||||
|
|
||||||
|
public function index(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
return ApiResponse::handle(
|
||||||
|
fn () => $this->service->index($request->only([
|
||||||
|
'search', 'ownership_type', 'status', 'per_page',
|
||||||
|
])),
|
||||||
|
__('message.fetched')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function show(int $id): JsonResponse
|
||||||
|
{
|
||||||
|
return ApiResponse::handle(
|
||||||
|
fn () => $this->service->show($id),
|
||||||
|
__('message.fetched')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
return ApiResponse::handle(
|
||||||
|
fn () => $this->service->store($request->all()),
|
||||||
|
__('message.created')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(Request $request, int $id): JsonResponse
|
||||||
|
{
|
||||||
|
return ApiResponse::handle(
|
||||||
|
fn () => $this->service->update($id, $request->all()),
|
||||||
|
__('message.updated')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy(int $id): JsonResponse
|
||||||
|
{
|
||||||
|
return ApiResponse::handle(
|
||||||
|
fn () => $this->service->destroy($id),
|
||||||
|
__('message.deleted')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function dropdown(): JsonResponse
|
||||||
|
{
|
||||||
|
return ApiResponse::handle(
|
||||||
|
fn () => $this->service->dropdown(),
|
||||||
|
__('message.fetched')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
66
app/Http/Controllers/V1/Vehicle/VehicleLogController.php
Normal file
66
app/Http/Controllers/V1/Vehicle/VehicleLogController.php
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\V1\Vehicle;
|
||||||
|
|
||||||
|
use App\Helpers\ApiResponse;
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Services\Vehicle\VehicleLogService;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class VehicleLogController extends Controller
|
||||||
|
{
|
||||||
|
public function __construct(private readonly VehicleLogService $service) {}
|
||||||
|
|
||||||
|
public function index(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
return ApiResponse::handle(
|
||||||
|
fn () => $this->service->index($request->only([
|
||||||
|
'search', 'vehicle_id', 'year', 'month', 'trip_type', 'per_page',
|
||||||
|
])),
|
||||||
|
__('message.fetched')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function show(int $id): JsonResponse
|
||||||
|
{
|
||||||
|
return ApiResponse::handle(
|
||||||
|
fn () => $this->service->show($id),
|
||||||
|
__('message.fetched')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
return ApiResponse::handle(
|
||||||
|
fn () => $this->service->store($request->all()),
|
||||||
|
__('message.created')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(Request $request, int $id): JsonResponse
|
||||||
|
{
|
||||||
|
return ApiResponse::handle(
|
||||||
|
fn () => $this->service->update($id, $request->all()),
|
||||||
|
__('message.updated')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy(int $id): JsonResponse
|
||||||
|
{
|
||||||
|
return ApiResponse::handle(
|
||||||
|
fn () => $this->service->destroy($id),
|
||||||
|
__('message.deleted')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function summary(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
return ApiResponse::handle(
|
||||||
|
fn () => $this->service->summary($request->only([
|
||||||
|
'vehicle_id', 'year', 'month',
|
||||||
|
])),
|
||||||
|
__('message.fetched')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\V1\Vehicle;
|
||||||
|
|
||||||
|
use App\Helpers\ApiResponse;
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Services\Vehicle\VehicleMaintenanceService;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class VehicleMaintenanceController extends Controller
|
||||||
|
{
|
||||||
|
public function __construct(private readonly VehicleMaintenanceService $service) {}
|
||||||
|
|
||||||
|
public function index(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
return ApiResponse::handle(
|
||||||
|
fn () => $this->service->index($request->only([
|
||||||
|
'search', 'vehicle_id', 'category', 'start_date', 'end_date', 'per_page',
|
||||||
|
])),
|
||||||
|
__('message.fetched')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function show(int $id): JsonResponse
|
||||||
|
{
|
||||||
|
return ApiResponse::handle(
|
||||||
|
fn () => $this->service->show($id),
|
||||||
|
__('message.fetched')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
return ApiResponse::handle(
|
||||||
|
fn () => $this->service->store($request->all()),
|
||||||
|
__('message.created')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(Request $request, int $id): JsonResponse
|
||||||
|
{
|
||||||
|
return ApiResponse::handle(
|
||||||
|
fn () => $this->service->update($id, $request->all()),
|
||||||
|
__('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 토큰 없이 접근 가능
|
// 화이트리스트(인증 예외 라우트) - Bearer 토큰 없이 접근 가능
|
||||||
$allowWithoutAuth = [
|
$allowWithoutAuth = [
|
||||||
'api/v1/login',
|
'api/v1/login',
|
||||||
|
'api/v1/token-login', // MNG → SAM 자동 로그인 (API Key만 필요)
|
||||||
'api/v1/signup',
|
'api/v1/signup',
|
||||||
'api/v1/register',
|
'api/v1/register',
|
||||||
'api/v1/refresh',
|
'api/v1/refresh',
|
||||||
|
|||||||
93
app/Http/Middleware/DemoLimitMiddleware.php
Normal file
93
app/Http/Middleware/DemoLimitMiddleware.php
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
|
use App\Models\Tenants\Tenant;
|
||||||
|
use Closure;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 데모 테넌트 제한 미들웨어
|
||||||
|
*
|
||||||
|
* - DEMO_SHOWCASE: 모든 쓰기 작업 차단 (읽기 전용)
|
||||||
|
* - DEMO_PARTNER / DEMO_TRIAL: 만료 체크 + 차단 기능 체크
|
||||||
|
*
|
||||||
|
* 기존 코드 영향 없음: 프로덕션 테넌트(STD/TPL/HQ)는 즉시 통과
|
||||||
|
*
|
||||||
|
* @see docs/features/sales/demo-tenant-policy.md
|
||||||
|
*/
|
||||||
|
class DemoLimitMiddleware
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 데모에서 차단하는 라우트 프리픽스 (외부 시스템 연동)
|
||||||
|
*/
|
||||||
|
private const BLOCKED_ROUTE_PREFIXES = [
|
||||||
|
'api/v1/barobill',
|
||||||
|
'api/v1/ecount',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function handle(Request $request, Closure $next): Response
|
||||||
|
{
|
||||||
|
$tenantId = app('tenant_id');
|
||||||
|
if (! $tenantId) {
|
||||||
|
return $next($request);
|
||||||
|
}
|
||||||
|
|
||||||
|
$tenant = Tenant::withoutGlobalScopes()->find($tenantId);
|
||||||
|
if (! $tenant || ! $tenant->isDemoTenant()) {
|
||||||
|
// 프로덕션 테넌트 → 즉시 통과 (기존 동작 유지)
|
||||||
|
return $next($request);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. 만료 체크 (파트너 데모, 고객 체험)
|
||||||
|
if ($tenant->isDemoExpired()) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => '체험 기간이 만료되었습니다. 정식 계약을 진행해 주세요.',
|
||||||
|
'error_code' => 'DEMO_EXPIRED',
|
||||||
|
], 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 쇼케이스 → 읽기 전용 (GET, HEAD, OPTIONS만 허용)
|
||||||
|
if ($tenant->isDemoShowcase() && ! $request->isMethodSafe()) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => '데모 환경에서는 조회만 가능합니다.',
|
||||||
|
'error_code' => 'DEMO_READ_ONLY',
|
||||||
|
], 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 읽기전용 옵션이 설정된 데모 테넌트
|
||||||
|
if ($tenant->isDemoReadOnly() && ! $request->isMethodSafe()) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => '데모 환경에서는 조회만 가능합니다.',
|
||||||
|
'error_code' => 'DEMO_READ_ONLY',
|
||||||
|
], 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 차단 기능 체크 (바로빌, 이카운트 등 외부 연동)
|
||||||
|
if ($this->isBlockedRoute($request)) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => '데모 환경에서 사용할 수 없는 기능입니다. 정식 계약 후 이용 가능합니다.',
|
||||||
|
'error_code' => 'DEMO_FEATURE_BLOCKED',
|
||||||
|
], 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $next($request);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function isBlockedRoute(Request $request): bool
|
||||||
|
{
|
||||||
|
$path = $request->path();
|
||||||
|
foreach (self::BLOCKED_ROUTE_PREFIXES as $prefix) {
|
||||||
|
if (str_starts_with($path, $prefix)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
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',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
35
app/Http/Requests/Demo/DemoTenantStoreRequest.php
Normal file
35
app/Http/Requests/Demo/DemoTenantStoreRequest.php
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests\Demo;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class DemoTenantStoreRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'company_name' => 'required|string|max:100',
|
||||||
|
'email' => 'required|email|max:255',
|
||||||
|
'duration_days' => 'sometimes|integer|min:7|max:60',
|
||||||
|
'preset' => 'sometimes|string|in:manufacturing',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function messages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'company_name.required' => '회사명은 필수입니다.',
|
||||||
|
'email.required' => '이메일은 필수입니다.',
|
||||||
|
'email.email' => '올바른 이메일 형식이 아닙니다.',
|
||||||
|
'duration_days.min' => '체험 기간은 최소 7일입니다.',
|
||||||
|
'duration_days.max' => '체험 기간은 최대 60일입니다.',
|
||||||
|
'preset.in' => '유효하지 않은 프리셋입니다.',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
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'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
53
app/Http/Requests/Finance/StoreVehiclePhotoRequest.php
Normal file
53
app/Http/Requests/Finance/StoreVehiclePhotoRequest.php
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests\Finance;
|
||||||
|
|
||||||
|
use App\Models\Commons\File;
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class StoreVehiclePhotoRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
$vehicleId = $this->route('id');
|
||||||
|
$currentCount = File::where('document_id', $vehicleId)
|
||||||
|
->where('document_type', 'corporate_vehicle')
|
||||||
|
->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.vehicle.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.*.productCategory' => 'nullable|string|in:SCREEN,STEEL',
|
||||||
'items.*.guideRailType' => 'nullable|string|in:wall,ceiling,floor,mixed',
|
'items.*.guideRailType' => 'nullable|string|in:wall,ceiling,floor,mixed',
|
||||||
'items.*.motorPower' => 'nullable|string|in:single,three',
|
'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.*.wingSize' => 'nullable|numeric|min:0|max:500',
|
||||||
'items.*.inspectionFee' => 'nullable|numeric|min:0',
|
'items.*.inspectionFee' => 'nullable|numeric|min:0',
|
||||||
|
|
||||||
@@ -45,7 +45,7 @@ public function rules(): array
|
|||||||
'items.*.PC' => 'nullable|string|in:SCREEN,STEEL',
|
'items.*.PC' => 'nullable|string|in:SCREEN,STEEL',
|
||||||
'items.*.GT' => 'nullable|string|in:wall,ceiling,floor,mixed',
|
'items.*.GT' => 'nullable|string|in:wall,ceiling,floor,mixed',
|
||||||
'items.*.MP' => 'nullable|string|in:single,three',
|
'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.*.WS' => 'nullable|numeric|min:0|max:500',
|
||||||
'items.*.INSP' => 'nullable|numeric|min:0',
|
'items.*.INSP' => 'nullable|numeric|min:0',
|
||||||
|
|
||||||
@@ -128,7 +128,7 @@ private function normalizeInputVariables(array $item): array
|
|||||||
'PC' => $item['productCategory'] ?? $item['PC'] ?? 'SCREEN',
|
'PC' => $item['productCategory'] ?? $item['PC'] ?? 'SCREEN',
|
||||||
'GT' => $item['guideRailType'] ?? $item['GT'] ?? 'wall',
|
'GT' => $item['guideRailType'] ?? $item['GT'] ?? 'wall',
|
||||||
'MP' => $item['motorPower'] ?? $item['MP'] ?? 'single',
|
'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),
|
'WS' => (float) ($item['wingSize'] ?? $item['WS'] ?? 50),
|
||||||
'INSP' => (float) ($item['inspectionFee'] ?? $item['INSP'] ?? 50000),
|
'INSP' => (float) ($item['inspectionFee'] ?? $item['INSP'] ?? 50000),
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ public function rules(): array
|
|||||||
'PC' => 'nullable|string|in:SCREEN,STEEL',
|
'PC' => 'nullable|string|in:SCREEN,STEEL',
|
||||||
'GT' => 'nullable|string|in:wall,ceiling,floor,mixed',
|
'GT' => 'nullable|string|in:wall,ceiling,floor,mixed',
|
||||||
'MP' => 'nullable|string|in:single,three',
|
'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',
|
'WS' => 'nullable|numeric|min:0|max:500',
|
||||||
'INSP' => 'nullable|numeric|min:0',
|
'INSP' => 'nullable|numeric|min:0',
|
||||||
|
|
||||||
@@ -82,7 +82,7 @@ public function getInputVariables(): array
|
|||||||
'PC' => $validated['PC'] ?? 'SCREEN',
|
'PC' => $validated['PC'] ?? 'SCREEN',
|
||||||
'GT' => $validated['GT'] ?? 'wall',
|
'GT' => $validated['GT'] ?? 'wall',
|
||||||
'MP' => $validated['MP'] ?? 'single',
|
'MP' => $validated['MP'] ?? 'single',
|
||||||
'CT' => $validated['CT'] ?? 'basic',
|
'CT' => $validated['CT'] ?? 'exposed',
|
||||||
'WS' => (float) ($validated['WS'] ?? 50),
|
'WS' => (float) ($validated['WS'] ?? 50),
|
||||||
'INSP' => (float) ($validated['INSP'] ?? 50000),
|
'INSP' => (float) ($validated['INSP'] ?? 50000),
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -20,18 +20,18 @@ public function rules(): array
|
|||||||
'issue_type' => ['required', 'string', Rule::in(TaxInvoice::ISSUE_TYPES)],
|
'issue_type' => ['required', 'string', Rule::in(TaxInvoice::ISSUE_TYPES)],
|
||||||
'direction' => ['required', 'string', Rule::in(TaxInvoice::DIRECTIONS)],
|
'direction' => ['required', 'string', Rule::in(TaxInvoice::DIRECTIONS)],
|
||||||
|
|
||||||
// 공급자 정보
|
// 공급자 정보 (매입 시 필수, 매출 시 선택)
|
||||||
'supplier_corp_num' => ['required', 'string', 'max:20'],
|
'supplier_corp_num' => ['required_if:direction,purchases', 'nullable', 'string', 'max:20'],
|
||||||
'supplier_corp_name' => ['required', 'string', 'max:100'],
|
'supplier_corp_name' => ['required_if:direction,purchases', 'nullable', 'string', 'max:100'],
|
||||||
'supplier_ceo_name' => ['nullable', 'string', 'max:50'],
|
'supplier_ceo_name' => ['nullable', 'string', 'max:50'],
|
||||||
'supplier_addr' => ['nullable', 'string', 'max:200'],
|
'supplier_addr' => ['nullable', 'string', 'max:200'],
|
||||||
'supplier_biz_type' => ['nullable', 'string', 'max:100'],
|
'supplier_biz_type' => ['nullable', 'string', 'max:100'],
|
||||||
'supplier_biz_class' => ['nullable', 'string', 'max:100'],
|
'supplier_biz_class' => ['nullable', 'string', 'max:100'],
|
||||||
'supplier_contact_id' => ['nullable', 'string', 'email', 'max:100'],
|
'supplier_contact_id' => ['nullable', 'string', 'email', 'max:100'],
|
||||||
|
|
||||||
// 공급받는자 정보
|
// 공급받는자 정보 (매출 시 필수, 매입 시 선택)
|
||||||
'buyer_corp_num' => ['required', 'string', 'max:20'],
|
'buyer_corp_num' => ['required_if:direction,sales', 'nullable', 'string', 'max:20'],
|
||||||
'buyer_corp_name' => ['required', 'string', 'max:100'],
|
'buyer_corp_name' => ['required_if:direction,sales', 'nullable', 'string', 'max:100'],
|
||||||
'buyer_ceo_name' => ['nullable', 'string', 'max:50'],
|
'buyer_ceo_name' => ['nullable', 'string', 'max:50'],
|
||||||
'buyer_addr' => ['nullable', 'string', 'max:200'],
|
'buyer_addr' => ['nullable', 'string', 'max:200'],
|
||||||
'buyer_biz_type' => ['nullable', 'string', 'max:100'],
|
'buyer_biz_type' => ['nullable', 'string', 'max:100'],
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
namespace App\Http\Requests\User;
|
namespace App\Http\Requests\User;
|
||||||
|
|
||||||
use Illuminate\Foundation\Http\FormRequest;
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
class SwitchTenantRequest extends FormRequest
|
class SwitchTenantRequest extends FormRequest
|
||||||
{
|
{
|
||||||
@@ -13,8 +14,23 @@ public function authorize(): bool
|
|||||||
|
|
||||||
public function rules(): array
|
public function rules(): array
|
||||||
{
|
{
|
||||||
|
$userId = app('api_user');
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'tenant_id' => 'required|integer|exists:tenants,id',
|
'tenant_id' => [
|
||||||
|
'required',
|
||||||
|
'integer',
|
||||||
|
Rule::exists('user_tenants', 'tenant_id')
|
||||||
|
->where('user_id', $userId)
|
||||||
|
->where('is_active', 1),
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function messages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'tenant_id.exists' => __('error.tenant_access_denied'),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,12 @@ public function rules(): array
|
|||||||
'code' => ['required', 'string', 'max:10'],
|
'code' => ['required', 'string', 'max:10'],
|
||||||
'name' => ['required', 'string', 'max:100'],
|
'name' => ['required', 'string', 'max:100'],
|
||||||
'category' => ['nullable', 'string', 'in:asset,liability,capital,revenue,expense'],
|
'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' => '계정과목 코드를 입력하세요.',
|
'code.required' => '계정과목 코드를 입력하세요.',
|
||||||
'name.required' => '계정과목명을 입력하세요.',
|
'name.required' => '계정과목명을 입력하세요.',
|
||||||
'category.in' => '유효한 분류를 선택하세요.',
|
'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' => ['nullable', 'array'],
|
||||||
'deductions.*.name' => ['required_with:deductions', 'string', 'max:50'],
|
'deductions.*.name' => ['required_with:deductions', 'string', 'max:50'],
|
||||||
'deductions.*.amount' => ['required_with:deductions', 'numeric', 'min:0'],
|
'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'],
|
'note' => ['nullable', 'string', 'max:1000'],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,14 @@ public function rules(): array
|
|||||||
'deductions' => ['nullable', 'array'],
|
'deductions' => ['nullable', 'array'],
|
||||||
'deductions.*.name' => ['required_with:deductions', 'string', 'max:50'],
|
'deductions.*.name' => ['required_with:deductions', 'string', 'max:50'],
|
||||||
'deductions.*.amount' => ['required_with:deductions', 'numeric', 'min:0'],
|
'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'],
|
'note' => ['nullable', 'string', 'max:1000'],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ public function rules(): array
|
|||||||
'remark' => ['nullable', 'string', 'max:1000'],
|
'remark' => ['nullable', 'string', 'max:1000'],
|
||||||
'manufacturer' => ['nullable', 'string', 'max:100'],
|
'manufacturer' => ['nullable', 'string', 'max:100'],
|
||||||
'material_no' => ['nullable', 'string', 'max:50'],
|
'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'],
|
'inspection_result' => ['nullable', 'string', 'max:20'],
|
||||||
'manufacturer' => ['nullable', 'string', 'max:100'],
|
'manufacturer' => ['nullable', 'string', 'max:100'],
|
||||||
'material_no' => ['nullable', 'string', 'max:50'],
|
'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
|
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
|
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()) {
|
if (! $this->exists()) {
|
||||||
abort(404, 'File not found in storage');
|
abort(404, 'File not found in storage');
|
||||||
}
|
}
|
||||||
|
|
||||||
return response()->download(
|
$fileName = $this->display_name ?? $this->original_name;
|
||||||
$this->getStoragePath(),
|
$mimeType = $this->mime_type ?? 'application/octet-stream';
|
||||||
$this->display_name ?? $this->original_name
|
$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
|
$this->stored_name ?? $this->file_name
|
||||||
);
|
);
|
||||||
|
|
||||||
// Move physical file
|
// Move physical file in R2
|
||||||
if (Storage::disk('tenant')->exists($this->file_path)) {
|
if (Storage::disk('r2')->exists($this->file_path)) {
|
||||||
Storage::disk('tenant')->move($this->file_path, $newPath);
|
Storage::disk('r2')->move($this->file_path, $newPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update DB
|
// Update DB
|
||||||
@@ -182,9 +198,9 @@ public function softDeleteFile(int $userId): void
|
|||||||
*/
|
*/
|
||||||
public function permanentDelete(): void
|
public function permanentDelete(): void
|
||||||
{
|
{
|
||||||
// Delete physical file
|
// Delete physical file from R2
|
||||||
if ($this->exists()) {
|
if ($this->exists()) {
|
||||||
Storage::disk('tenant')->delete($this->file_path);
|
Storage::disk('r2')->delete($this->file_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decrement tenant storage
|
// 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\Members\User::class, 'manager_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function subManager(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(\App\Models\Members\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\Members\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\Members\User::class, 'repaired_by');
|
||||||
|
}
|
||||||
|
}
|
||||||
40
app/Models/Finance/CorporateVehicle.php
Normal file
40
app/Models/Finance/CorporateVehicle.php
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models\Finance;
|
||||||
|
|
||||||
|
use App\Models\Commons\File;
|
||||||
|
use App\Traits\BelongsToTenant;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
|
|
||||||
|
class CorporateVehicle extends Model
|
||||||
|
{
|
||||||
|
use BelongsToTenant, SoftDeletes;
|
||||||
|
|
||||||
|
protected $table = 'corporate_vehicles';
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'tenant_id',
|
||||||
|
'plate_number',
|
||||||
|
'model',
|
||||||
|
'vehicle_type',
|
||||||
|
'ownership_type',
|
||||||
|
'mileage',
|
||||||
|
'options',
|
||||||
|
'is_active',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'mileage' => 'integer',
|
||||||
|
'options' => 'array',
|
||||||
|
'is_active' => 'boolean',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function photos(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(File::class, 'document_id')
|
||||||
|
->where('document_type', 'corporate_vehicle')
|
||||||
|
->orderBy('id');
|
||||||
|
}
|
||||||
|
}
|
||||||
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');
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user