Compare commits
133 Commits
ad93743bdc
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bba8f6c0a0 | ||
|
|
ba8fc0834c | ||
|
|
ecfe389420 | ||
|
|
e83d0e90ff | ||
|
|
d1c65f5465 | ||
|
|
17a0d2f98d | ||
|
|
a96fd254e5 | ||
|
|
b10713344a | ||
|
|
b60e44ea3a | ||
| 9358c4112e | |||
| 0863afc8d0 | |||
| afc31be642 | |||
| e5da452fde | |||
| 5e65cbc93e | |||
| 8821509c99 | |||
|
|
7a9b800413 | ||
|
|
c11ac7867c | ||
|
|
269a17b49c | ||
| 7083057d59 | |||
| 13d91b7ab4 | |||
|
|
ead546e268 | ||
|
|
921f1ecba7 | ||
|
|
8404f29bca | ||
|
|
053323c144 | ||
|
|
750776d5c8 | ||
|
|
ae73275cf9 | ||
|
|
407afe38e4 | ||
| 57133541d0 | |||
|
|
a7f98ccdf5 | ||
|
|
9d2333bfb1 | ||
|
|
6e6843fd67 | ||
|
|
d8560d889c | ||
|
|
9d95b2c373 | ||
|
|
6b9673d21a | ||
|
|
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 | ||
| 3ac64d5b76 | |||
| 2231c9a48f | |||
| ff8553055c | |||
| f2eede6e3a | |||
| c5d5b5d076 | |||
| 5ebf940873 | |||
| 293330c418 | |||
| a845f52fc0 | |||
| 0f26ea546a | |||
| 3600c7b12b | |||
| a6e29bc1f3 | |||
| 0aa0a8592d | |||
| 38c2402771 | |||
| 59d13eeb9f | |||
| 2df8ecf765 |
@@ -54,4 +54,4 @@ ## 관련 파일
|
|||||||
|
|
||||||
- `api/app/Services/ComprehensiveAnalysisService.php`
|
- `api/app/Services/ComprehensiveAnalysisService.php`
|
||||||
- `api/database/seeders/ComprehensiveAnalysisSeeder.php`
|
- `api/database/seeders/ComprehensiveAnalysisSeeder.php`
|
||||||
- `docs/plans/react-mock-remaining-tasks.md`
|
- `docs/dev/dev_plans/react-mock-remaining-tasks.md`
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ ## Phase 구성
|
|||||||
- Phase 5: MNG 관리자 패널 (4항목) — mng/ /system/alerts
|
- Phase 5: MNG 관리자 패널 (4항목) — mng/ /system/alerts
|
||||||
|
|
||||||
## 핵심 파일
|
## 핵심 파일
|
||||||
- 계획 문서: docs/plans/db-backup-system-plan.md
|
- 계획 문서: docs/dev/dev_plans/db-backup-system-plan.md
|
||||||
- 개발서버: 114.203.209.83 (SSH: hskwon)
|
- 개발서버: 114.203.209.83 (SSH: hskwon)
|
||||||
- DB: sam (메인) + sam_stat (통계)
|
- DB: sam (메인) + sam_stat (통계)
|
||||||
- Slack 웹훅: api/.env → LOG_SLACK_WEBHOOK_URL (이미 설정됨)
|
- Slack 웹훅: api/.env → LOG_SLACK_WEBHOOK_URL (이미 설정됨)
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ ### 생성된 파일
|
|||||||
| 파일 | 설명 |
|
| 파일 | 설명 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| `app/Http/Requests/Quote/QuoteBomBulkCalculateRequest.php` | 다건 BOM 산출 FormRequest |
|
| `app/Http/Requests/Quote/QuoteBomBulkCalculateRequest.php` | 다건 BOM 산출 FormRequest |
|
||||||
| `api/docs/changes/20260102_1300_quote_bom_bulk_calculation.md` | 변경 내용 문서 |
|
| `api/docs/dev/changes/20260102_1300_quote_bom_bulk_calculation.md` | 변경 내용 문서 |
|
||||||
|
|
||||||
### 수정된 파일
|
### 수정된 파일
|
||||||
| 파일 | 설명 |
|
| 파일 | 설명 |
|
||||||
@@ -93,9 +93,9 @@ ### QuoteCalculationService::calculateBomBulk()
|
|||||||
- 개별 품목 실패가 전체에 영향 없음 (예외 처리)
|
- 개별 품목 실패가 전체에 영향 없음 (예외 처리)
|
||||||
|
|
||||||
## 관련 문서
|
## 관련 문서
|
||||||
- 계획 문서: `docs/plans/quote-calculation-api-plan.md`
|
- 계획 문서: `docs/dev/dev_plans/quote-calculation-api-plan.md`
|
||||||
- Phase 1.1 문서: `docs/changes/20260102_quote_bom_calculation_api.md`
|
- Phase 1.1 문서: `docs/dev/changes/20260102_quote_bom_calculation_api.md`
|
||||||
- Phase 1.2 문서: `docs/changes/20260102_1300_quote_bom_bulk_calculation.md`
|
- Phase 1.2 문서: `docs/dev/changes/20260102_1300_quote_bom_bulk_calculation.md`
|
||||||
|
|
||||||
## 다음 단계
|
## 다음 단계
|
||||||
- React 프론트엔드에서 `/calculate/bom/bulk` API 연동
|
- React 프론트엔드에서 `/calculate/bom/bulk` API 연동
|
||||||
|
|||||||
@@ -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-04 22:33:37
|
> **자동 생성**: 2026-03-17 15:29:06
|
||||||
> **소스**: 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,47 @@ ### 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`
|
||||||
|
|
||||||
|
- **manager()**: belongsTo → `users`
|
||||||
|
- **subManager()**: belongsTo → `users`
|
||||||
|
- **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`
|
||||||
|
- **inspector()**: belongsTo → `users`
|
||||||
|
- **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`
|
||||||
|
- **repairer()**: belongsTo → `users`
|
||||||
|
|
||||||
### estimates
|
### estimates
|
||||||
**모델**: `App\Models\Estimate\Estimate`
|
**모델**: `App\Models\Estimate\Estimate`
|
||||||
|
|
||||||
@@ -330,6 +433,12 @@ ### file_share_links
|
|||||||
- **file()**: belongsTo → `files`
|
- **file()**: belongsTo → `files`
|
||||||
- **tenant()**: belongsTo → `tenants`
|
- **tenant()**: belongsTo → `tenants`
|
||||||
|
|
||||||
|
### corporate_vehicles
|
||||||
|
**모델**: `App\Models\Tenants\CorporateVehicle`
|
||||||
|
|
||||||
|
- **logs()**: hasMany → `vehicle_logs`
|
||||||
|
- **maintenances()**: hasMany → `vehicle_maintenances`
|
||||||
|
|
||||||
### folders
|
### folders
|
||||||
**모델**: `App\Models\Folder`
|
**모델**: `App\Models\Folder`
|
||||||
|
|
||||||
@@ -614,6 +723,11 @@ ### process_steps
|
|||||||
|
|
||||||
- **process()**: belongsTo → `processes`
|
- **process()**: belongsTo → `processes`
|
||||||
|
|
||||||
|
### bending_item_mappings
|
||||||
|
**모델**: `App\Models\Production\BendingItemMapping`
|
||||||
|
|
||||||
|
- **item()**: belongsTo → `items`
|
||||||
|
|
||||||
### work_orders
|
### work_orders
|
||||||
**모델**: `App\Models\Production\WorkOrder`
|
**모델**: `App\Models\Production\WorkOrder`
|
||||||
|
|
||||||
@@ -734,6 +848,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`
|
||||||
|
|
||||||
@@ -753,6 +897,39 @@ ### lot_sales
|
|||||||
|
|
||||||
- **lot()**: belongsTo → `lots`
|
- **lot()**: belongsTo → `lots`
|
||||||
|
|
||||||
|
### performance_reports
|
||||||
|
**모델**: `App\Models\Qualitys\PerformanceReport`
|
||||||
|
|
||||||
|
- **qualityDocument()**: belongsTo → `quality_documents`
|
||||||
|
- **confirmer()**: belongsTo → `users`
|
||||||
|
- **creator()**: belongsTo → `users`
|
||||||
|
|
||||||
|
### quality_documents
|
||||||
|
**모델**: `App\Models\Qualitys\QualityDocument`
|
||||||
|
|
||||||
|
- **client()**: belongsTo → `clients`
|
||||||
|
- **inspector()**: belongsTo → `users`
|
||||||
|
- **creator()**: belongsTo → `users`
|
||||||
|
- **documentOrders()**: hasMany → `quality_document_orders`
|
||||||
|
- **locations()**: hasMany → `quality_document_locations`
|
||||||
|
- **performanceReport()**: hasOne → `performance_reports`
|
||||||
|
- **file()**: hasOne → `files`
|
||||||
|
|
||||||
|
### quality_document_locations
|
||||||
|
**모델**: `App\Models\Qualitys\QualityDocumentLocation`
|
||||||
|
|
||||||
|
- **qualityDocument()**: belongsTo → `quality_documents`
|
||||||
|
- **qualityDocumentOrder()**: belongsTo → `quality_document_orders`
|
||||||
|
- **orderItem()**: belongsTo → `order_items`
|
||||||
|
- **document()**: belongsTo → `documents`
|
||||||
|
|
||||||
|
### quality_document_orders
|
||||||
|
**모델**: `App\Models\Qualitys\QualityDocumentOrder`
|
||||||
|
|
||||||
|
- **qualityDocument()**: belongsTo → `quality_documents`
|
||||||
|
- **order()**: belongsTo → `orders`
|
||||||
|
- **locations()**: hasMany → `quality_document_locations`
|
||||||
|
|
||||||
### quotes
|
### quotes
|
||||||
**모델**: `App\Models\Quote\Quote`
|
**모델**: `App\Models\Quote\Quote`
|
||||||
|
|
||||||
@@ -806,6 +983,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`
|
||||||
|
|
||||||
@@ -825,14 +1007,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`
|
||||||
|
|
||||||
@@ -851,6 +1043,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`
|
||||||
@@ -929,6 +1122,16 @@ ### expense_accounts
|
|||||||
|
|
||||||
- **vendor()**: belongsTo → `clients`
|
- **vendor()**: belongsTo → `clients`
|
||||||
|
|
||||||
|
### journal_entrys
|
||||||
|
**모델**: `App\Models\Tenants\JournalEntry`
|
||||||
|
|
||||||
|
- **lines()**: hasMany → `journal_entry_lines`
|
||||||
|
|
||||||
|
### journal_entry_lines
|
||||||
|
**모델**: `App\Models\Tenants\JournalEntryLine`
|
||||||
|
|
||||||
|
- **journalEntry()**: belongsTo → `journal_entries`
|
||||||
|
|
||||||
### leaves
|
### leaves
|
||||||
**모델**: `App\Models\Tenants\Leave`
|
**모델**: `App\Models\Tenants\Leave`
|
||||||
|
|
||||||
@@ -962,6 +1165,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`
|
||||||
|
|
||||||
@@ -1004,6 +1212,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
|
||||||
@@ -1039,6 +1248,7 @@ ### shipments
|
|||||||
|
|
||||||
- **order()**: belongsTo → `orders`
|
- **order()**: belongsTo → `orders`
|
||||||
- **workOrder()**: belongsTo → `work_orders`
|
- **workOrder()**: belongsTo → `work_orders`
|
||||||
|
- **client()**: belongsTo → `clients`
|
||||||
- **creator()**: belongsTo → `users`
|
- **creator()**: belongsTo → `users`
|
||||||
- **updater()**: belongsTo → `users`
|
- **updater()**: belongsTo → `users`
|
||||||
- **items()**: hasMany → `shipment_items`
|
- **items()**: hasMany → `shipment_items`
|
||||||
@@ -1049,6 +1259,7 @@ ### shipment_items
|
|||||||
|
|
||||||
- **shipment()**: belongsTo → `shipments`
|
- **shipment()**: belongsTo → `shipments`
|
||||||
- **stockLot()**: belongsTo → `stock_lots`
|
- **stockLot()**: belongsTo → `stock_lots`
|
||||||
|
- **orderItem()**: belongsTo → `order_items`
|
||||||
|
|
||||||
### shipment_vehicle_dispatchs
|
### shipment_vehicle_dispatchs
|
||||||
**모델**: `App\Models\Tenants\ShipmentVehicleDispatch`
|
**모델**: `App\Models\Tenants\ShipmentVehicleDispatch`
|
||||||
@@ -1125,6 +1336,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`
|
||||||
|
|
||||||
@@ -1155,6 +1371,16 @@ ### today_issues
|
|||||||
- **reader()**: belongsTo → `users`
|
- **reader()**: belongsTo → `users`
|
||||||
- **targetUser()**: belongsTo → `users`
|
- **targetUser()**: belongsTo → `users`
|
||||||
|
|
||||||
|
### vehicle_logs
|
||||||
|
**모델**: `App\Models\Tenants\VehicleLog`
|
||||||
|
|
||||||
|
- **vehicle()**: belongsTo → `corporate_vehicles`
|
||||||
|
|
||||||
|
### vehicle_maintenances
|
||||||
|
**모델**: `App\Models\Tenants\VehicleMaintenance`
|
||||||
|
|
||||||
|
- **vehicle()**: belongsTo → `corporate_vehicles`
|
||||||
|
|
||||||
### withdrawals
|
### withdrawals
|
||||||
**모델**: `App\Models\Tenants\Withdrawal`
|
**모델**: `App\Models\Tenants\Withdrawal`
|
||||||
|
|
||||||
|
|||||||
353
app/Console/Commands/BendingFillOptions.php
Normal file
353
app/Console/Commands/BendingFillOptions.php
Normal file
@@ -0,0 +1,353 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use Illuminate\Console\Attributes\AsCommand;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* BD-* 품목의 options 속성 보강
|
||||||
|
*
|
||||||
|
* 1단계: BD-PREFIX-LEN 패턴(112건)에서 prefix/length 자동 추출
|
||||||
|
* 2단계: BD-한글 패턴(58건)에 item_sep/item_bending 등 분류 속성 추가
|
||||||
|
*
|
||||||
|
* 실행: php artisan bending:fill-options [--dry-run] [--tenant_id=287]
|
||||||
|
*/
|
||||||
|
#[AsCommand(name: 'bending:fill-options', description: 'BD-* 품목 options 속성 보강 (prefix/length + 분류 속성)')]
|
||||||
|
class BendingFillOptions extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'bending:fill-options
|
||||||
|
{--tenant_id=287 : Target tenant ID}
|
||||||
|
{--dry-run : 실제 저장 없이 미리보기}';
|
||||||
|
|
||||||
|
// PREFIX → 분류 속성 매핑
|
||||||
|
private const PREFIX_META = [
|
||||||
|
// 가이드레일 (벽면)
|
||||||
|
'RS' => ['item_sep' => '스크린', 'item_bending' => '가이드레일', 'item_name' => 'SUS마감재', 'material' => 'SUS 1.2T'],
|
||||||
|
'RE' => ['item_sep' => '스크린', 'item_bending' => '가이드레일', 'item_name' => 'EGI마감재', 'material' => 'EGI 1.55T'],
|
||||||
|
'RM' => ['item_sep' => '스크린', 'item_bending' => '가이드레일', 'item_name' => '본체', 'material' => 'EGI 1.55T'],
|
||||||
|
'RC' => ['item_sep' => '스크린', 'item_bending' => '가이드레일', 'item_name' => 'C형', 'material' => 'EGI 1.55T'],
|
||||||
|
'RD' => ['item_sep' => '스크린', 'item_bending' => '가이드레일', 'item_name' => 'D형', 'material' => 'EGI 1.55T'],
|
||||||
|
'RT' => ['item_sep' => '철재', 'item_bending' => '가이드레일', 'item_name' => '본체(철재)', 'material' => 'EGI 1.55T'],
|
||||||
|
// 가이드레일 (측면)
|
||||||
|
'SS' => ['item_sep' => '스크린', 'item_bending' => '가이드레일', 'item_name' => 'SUS마감재', 'material' => 'SUS 1.2T'],
|
||||||
|
'SE' => ['item_sep' => '스크린', 'item_bending' => '가이드레일', 'item_name' => 'EGI마감재', 'material' => 'EGI 1.55T'],
|
||||||
|
'SM' => ['item_sep' => '스크린', 'item_bending' => '가이드레일', 'item_name' => '본체', 'material' => 'EGI 1.55T'],
|
||||||
|
'SC' => ['item_sep' => '스크린', 'item_bending' => '가이드레일', 'item_name' => 'C형', 'material' => 'EGI 1.55T'],
|
||||||
|
'SD' => ['item_sep' => '스크린', 'item_bending' => '가이드레일', 'item_name' => 'D형', 'material' => 'EGI 1.55T'],
|
||||||
|
'ST' => ['item_sep' => '철재', 'item_bending' => '가이드레일', 'item_name' => '본체(철재)', 'material' => 'EGI 1.55T'],
|
||||||
|
'SU' => ['item_sep' => '스크린', 'item_bending' => '가이드레일', 'item_name' => 'SUS마감재2', 'material' => 'SUS 1.2T'],
|
||||||
|
// 하단마감재
|
||||||
|
'BE' => ['item_sep' => '스크린', 'item_bending' => '하단마감재', 'item_name' => '하단마감재', 'material' => 'EGI 1.55T'],
|
||||||
|
'BS' => ['item_sep' => '스크린', 'item_bending' => '하단마감재', 'item_name' => '하단마감재', 'material' => 'SUS 1.5T'],
|
||||||
|
'TS' => ['item_sep' => '철재', 'item_bending' => '하단마감재', 'item_name' => '하단마감재(철재)', 'material' => 'SUS 1.2T'],
|
||||||
|
'LA' => ['item_sep' => '스크린', 'item_bending' => 'L-BAR', 'item_name' => 'L-Bar', 'material' => 'EGI 1.55T'],
|
||||||
|
'HH' => ['item_sep' => '스크린', 'item_bending' => '보강평철', 'item_name' => '보강평철', 'material' => 'EGI 1.55T'],
|
||||||
|
// 셔터박스
|
||||||
|
'CF' => ['item_sep' => '스크린', 'item_bending' => '케이스', 'item_name' => '전면부', 'material' => 'EGI 1.55T'],
|
||||||
|
'CL' => ['item_sep' => '스크린', 'item_bending' => '케이스', 'item_name' => '린텔부', 'material' => 'EGI 1.55T'],
|
||||||
|
'CP' => ['item_sep' => '스크린', 'item_bending' => '케이스', 'item_name' => '점검구', 'material' => 'EGI 1.55T'],
|
||||||
|
'CB' => ['item_sep' => '스크린', 'item_bending' => '케이스', 'item_name' => '후면코너부', 'material' => 'EGI 1.55T'],
|
||||||
|
// 연기차단재
|
||||||
|
'GI' => ['item_sep' => '스크린', 'item_bending' => '연기차단재', 'item_name' => '연기차단재', 'material' => '화이바원단'],
|
||||||
|
// 공용
|
||||||
|
'XX' => ['item_sep' => '스크린', 'item_bending' => '공용', 'item_name' => '하부BASE/상부덮개/마구리', 'material' => 'EGI 1.55T'],
|
||||||
|
'YY' => ['item_sep' => '스크린', 'item_bending' => '별도마감', 'item_name' => '별도SUS마감', 'material' => 'SUS 1.2T'],
|
||||||
|
];
|
||||||
|
|
||||||
|
// 한글 패턴 → 분류 매핑
|
||||||
|
private const KOREAN_PATTERN_META = [
|
||||||
|
'BD-가이드레일' => ['item_sep' => null, 'item_bending' => '가이드레일'],
|
||||||
|
'BD-케이스' => ['item_sep' => null, 'item_bending' => '케이스'],
|
||||||
|
'BD-마구리' => ['item_sep' => null, 'item_bending' => '마구리'],
|
||||||
|
'BD-하단마감재' => ['item_sep' => null, 'item_bending' => '하단마감재'],
|
||||||
|
'BD-L-BAR' => ['item_sep' => '스크린', 'item_bending' => 'L-BAR'],
|
||||||
|
'BD-보강평철' => ['item_sep' => '스크린', 'item_bending' => '보강평철'],
|
||||||
|
];
|
||||||
|
|
||||||
|
private const LENGTH_MAP = [
|
||||||
|
'02' => 200, '12' => 1219, '24' => 2438, '30' => 3000,
|
||||||
|
'35' => 3500, '40' => 4000, '41' => 4150, '42' => 4200,
|
||||||
|
'43' => 4300, '53' => 3000, '54' => 4000, '83' => 3000, '84' => 4000,
|
||||||
|
];
|
||||||
|
|
||||||
|
private array $stats = [
|
||||||
|
'total' => 0,
|
||||||
|
'prefix_len_filled' => 0,
|
||||||
|
'korean_filled' => 0,
|
||||||
|
'already_complete' => 0,
|
||||||
|
'unknown_pattern' => 0,
|
||||||
|
];
|
||||||
|
|
||||||
|
public function handle(): int
|
||||||
|
{
|
||||||
|
$tenantId = (int) $this->option('tenant_id');
|
||||||
|
$dryRun = $this->option('dry-run');
|
||||||
|
|
||||||
|
$this->info('=== BD-* 품목 options 보강 ===');
|
||||||
|
$this->info('Tenant: '.$tenantId.' | Mode: '.($dryRun ? 'DRY-RUN' : 'LIVE'));
|
||||||
|
$this->newLine();
|
||||||
|
|
||||||
|
// BD-* 전체 품목 조회
|
||||||
|
$items = DB::table('items')
|
||||||
|
->where('tenant_id', $tenantId)
|
||||||
|
->where('code', 'like', 'BD-%')
|
||||||
|
->whereNull('deleted_at')
|
||||||
|
->select('id', 'code', 'name', 'options')
|
||||||
|
->orderBy('code')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
$this->stats['total'] = $items->count();
|
||||||
|
$this->info("BD-* 품목: {$items->count()}건");
|
||||||
|
$this->newLine();
|
||||||
|
|
||||||
|
foreach ($items as $item) {
|
||||||
|
$options = json_decode($item->options ?? '{}', true) ?: [];
|
||||||
|
$code = $item->code;
|
||||||
|
$newOptions = $this->resolveOptions($code, $item->name, $options);
|
||||||
|
|
||||||
|
if ($newOptions === null) {
|
||||||
|
$this->stats['unknown_pattern']++;
|
||||||
|
$this->warn(" ❓ 미인식 패턴: {$code}");
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 변경 필요 여부 확인
|
||||||
|
$merged = array_merge($options, $newOptions);
|
||||||
|
if ($merged == $options) {
|
||||||
|
$this->stats['already_complete']++;
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $dryRun) {
|
||||||
|
$encoded = json_encode($merged, JSON_UNESCAPED_UNICODE);
|
||||||
|
if ($encoded === false) {
|
||||||
|
$this->error(" ❌ JSON 인코딩 실패: {$code} — ".json_last_error_msg());
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
DB::table('items')
|
||||||
|
->where('id', $item->id)
|
||||||
|
->update([
|
||||||
|
'options' => $encoded,
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$pattern = $this->detectPattern($code);
|
||||||
|
if ($pattern === 'prefix_len') {
|
||||||
|
$this->stats['prefix_len_filled']++;
|
||||||
|
} else {
|
||||||
|
$this->stats['korean_filled']++;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->line(" ✅ {$code}: +".implode(', ', array_keys($newOptions)));
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->showStats($dryRun);
|
||||||
|
|
||||||
|
return self::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 코드에서 options 속성 추출
|
||||||
|
*/
|
||||||
|
private function resolveOptions(string $code, string $name, array $existing): ?array
|
||||||
|
{
|
||||||
|
$new = [];
|
||||||
|
|
||||||
|
// item_category 보장
|
||||||
|
if (empty($existing['item_category'])) {
|
||||||
|
// item_category는 items 테이블 컬럼이므로 여기서는 skip
|
||||||
|
}
|
||||||
|
|
||||||
|
// 패턴 A: BD-PREFIX-LEN (예: BD-RS-30)
|
||||||
|
if (preg_match('/^BD-([A-Z]{2})-(\d{2})$/', $code, $m)) {
|
||||||
|
$prefix = $m[1];
|
||||||
|
$lengthCode = $m[2];
|
||||||
|
|
||||||
|
// prefix/length 기본값
|
||||||
|
if (empty($existing['prefix'])) {
|
||||||
|
$new['prefix'] = $prefix;
|
||||||
|
}
|
||||||
|
if (empty($existing['length_code'])) {
|
||||||
|
$new['length_code'] = $lengthCode;
|
||||||
|
}
|
||||||
|
if (empty($existing['length_mm']) && isset(self::LENGTH_MAP[$lengthCode])) {
|
||||||
|
$new['length_mm'] = self::LENGTH_MAP[$lengthCode];
|
||||||
|
}
|
||||||
|
|
||||||
|
// PREFIX 기반 분류 속성
|
||||||
|
$meta = self::PREFIX_META[$prefix] ?? null;
|
||||||
|
if ($meta) {
|
||||||
|
foreach ($meta as $key => $value) {
|
||||||
|
if (empty($existing[$key])) {
|
||||||
|
$new[$key] = $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $new;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 특수 코드 (패턴 미준수)
|
||||||
|
$specialCodes = [
|
||||||
|
'BD-가이드레일용 연기차단재' => ['item_bending' => '연기차단재'],
|
||||||
|
'BD-케이스용 연기차단재' => ['item_bending' => '연기차단재'],
|
||||||
|
];
|
||||||
|
if (isset($specialCodes[$code])) {
|
||||||
|
foreach ($specialCodes[$code] as $key => $value) {
|
||||||
|
if (empty($existing[$key])) {
|
||||||
|
$new[$key] = $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $new;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 패턴 B~G: 한글 패턴
|
||||||
|
foreach (self::KOREAN_PATTERN_META as $patternPrefix => $meta) {
|
||||||
|
// 정확한 접두사+구분자 매칭 (BD-케이스-xxx는 O, BD-케이스용xxx는 X)
|
||||||
|
if ($code === $patternPrefix || str_starts_with($code, $patternPrefix.'-')) {
|
||||||
|
// 분류 속성
|
||||||
|
foreach ($meta as $key => $value) {
|
||||||
|
if ($value !== null && empty($existing[$key])) {
|
||||||
|
$new[$key] = $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 한글 패턴별 추가 파싱
|
||||||
|
$this->parseKoreanPattern($code, $patternPrefix, $existing, $new);
|
||||||
|
|
||||||
|
return $new;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 한글 패턴에서 모델/재질/규격 추출
|
||||||
|
*/
|
||||||
|
private function parseKoreanPattern(string $code, string $patternPrefix, array $existing, array &$new): void
|
||||||
|
{
|
||||||
|
$suffix = substr($code, strlen($patternPrefix) + 1); // "-" 제거
|
||||||
|
$parts = explode('-', $suffix);
|
||||||
|
|
||||||
|
switch ($patternPrefix) {
|
||||||
|
case 'BD-가이드레일':
|
||||||
|
// BD-가이드레일-KSS01-SUS-120*70
|
||||||
|
if (count($parts) >= 3) {
|
||||||
|
if (empty($existing['model_name'])) {
|
||||||
|
$new['model_name'] = $parts[0];
|
||||||
|
}
|
||||||
|
if (empty($existing['material'])) {
|
||||||
|
$material = $parts[1];
|
||||||
|
$new['material'] = str_contains($material, 'SUS') ? 'SUS 1.2T' : 'EGI 1.55T';
|
||||||
|
}
|
||||||
|
if (empty($existing['item_spec'])) {
|
||||||
|
$new['item_spec'] = $parts[2];
|
||||||
|
}
|
||||||
|
// item_sep 추론 (KTE → 철재)
|
||||||
|
if (empty($existing['item_sep'])) {
|
||||||
|
$new['item_sep'] = str_starts_with($parts[0], 'KTE') ? '철재' : '스크린';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'BD-하단마감재':
|
||||||
|
// BD-하단마감재-KSS01-SUS-60*40
|
||||||
|
if (count($parts) >= 3) {
|
||||||
|
if (empty($existing['model_name'])) {
|
||||||
|
$new['model_name'] = $parts[0];
|
||||||
|
}
|
||||||
|
if (empty($existing['material'])) {
|
||||||
|
$material = $parts[1];
|
||||||
|
$new['material'] = str_contains($material, 'SUS') ? 'SUS 1.5T' : 'EGI 1.55T';
|
||||||
|
}
|
||||||
|
if (empty($existing['item_spec'])) {
|
||||||
|
$new['item_spec'] = $parts[2];
|
||||||
|
}
|
||||||
|
if (empty($existing['item_sep'])) {
|
||||||
|
$new['item_sep'] = str_starts_with($parts[0], 'KTE') ? '철재' : '스크린';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'BD-케이스':
|
||||||
|
// BD-케이스-650*550
|
||||||
|
if (count($parts) >= 1 && ! empty($parts[0])) {
|
||||||
|
if (empty($existing['item_spec'])) {
|
||||||
|
$new['item_spec'] = $parts[0];
|
||||||
|
}
|
||||||
|
// 케이스는 대부분 철재
|
||||||
|
if (empty($existing['item_sep'])) {
|
||||||
|
$new['item_sep'] = '철재';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'BD-마구리':
|
||||||
|
// BD-마구리-655*505
|
||||||
|
if (count($parts) >= 1 && ! empty($parts[0])) {
|
||||||
|
if (empty($existing['item_spec'])) {
|
||||||
|
$new['item_spec'] = $parts[0];
|
||||||
|
}
|
||||||
|
if (empty($existing['item_sep'])) {
|
||||||
|
$new['item_sep'] = '철재';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'BD-L-BAR':
|
||||||
|
// BD-L-BAR-KSS01-17*60
|
||||||
|
if (count($parts) >= 2) {
|
||||||
|
if (empty($existing['model_name'])) {
|
||||||
|
$new['model_name'] = $parts[0];
|
||||||
|
}
|
||||||
|
if (empty($existing['item_spec'])) {
|
||||||
|
$new['item_spec'] = $parts[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'BD-보강평철':
|
||||||
|
// BD-보강평철-50
|
||||||
|
if (count($parts) >= 1 && ! empty($parts[0])) {
|
||||||
|
if (empty($existing['item_spec'])) {
|
||||||
|
$new['item_spec'] = $parts[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function detectPattern(string $code): string
|
||||||
|
{
|
||||||
|
return preg_match('/^BD-[A-Z]{2}-\d{2}$/', $code) ? 'prefix_len' : 'korean';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function showStats(bool $dryRun): void
|
||||||
|
{
|
||||||
|
$this->newLine();
|
||||||
|
$this->info('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||||||
|
$this->info('📊 결과'.($dryRun ? ' (DRY-RUN)' : ''));
|
||||||
|
$this->info('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||||||
|
$this->info(" 전체 BD-* 품목: {$this->stats['total']}건");
|
||||||
|
$this->info(" PREFIX-LEN 업데이트: {$this->stats['prefix_len_filled']}건");
|
||||||
|
$this->info(" 한글 패턴 업데이트: {$this->stats['korean_filled']}건");
|
||||||
|
$this->info(" 이미 완료: {$this->stats['already_complete']}건");
|
||||||
|
if ($this->stats['unknown_pattern'] > 0) {
|
||||||
|
$this->warn(" 미인식 패턴: {$this->stats['unknown_pattern']}건");
|
||||||
|
}
|
||||||
|
$this->info('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||||||
|
|
||||||
|
if ($dryRun) {
|
||||||
|
$this->newLine();
|
||||||
|
$this->info('🔍 DRY-RUN 완료. --dry-run 제거하면 실제 반영됩니다.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
169
app/Console/Commands/BendingImportImages.php
Normal file
169
app/Console/Commands/BendingImportImages.php
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use App\Models\Commons\File;
|
||||||
|
use Illuminate\Console\Attributes\AsCommand;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Http;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 레거시 5130 절곡 이미지 → SAM R2 + files 테이블 마이그레이션
|
||||||
|
*
|
||||||
|
* 소스: https://5130.codebridge-x.com/bending/img/{imgdata}
|
||||||
|
* 대상: R2 저장 + files 테이블 + items.options 업데이트
|
||||||
|
*/
|
||||||
|
#[AsCommand(name: 'bending:import-images', description: '레거시 절곡 이미지 → R2 마이그레이션')]
|
||||||
|
class BendingImportImages extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'bending:import-images
|
||||||
|
{--tenant_id=287 : Target tenant ID}
|
||||||
|
{--dry-run : 실제 저장 없이 미리보기}
|
||||||
|
{--source=https://5130.codebridge-x.com/bending/img : 이미지 소스 URL}';
|
||||||
|
|
||||||
|
private int $uploaded = 0;
|
||||||
|
|
||||||
|
private int $skipped = 0;
|
||||||
|
|
||||||
|
private int $failed = 0;
|
||||||
|
|
||||||
|
public function handle(): int
|
||||||
|
{
|
||||||
|
$tenantId = (int) $this->option('tenant_id');
|
||||||
|
$dryRun = $this->option('dry-run');
|
||||||
|
$sourceBase = rtrim($this->option('source'), '/');
|
||||||
|
|
||||||
|
$this->info('=== 레거시 절곡 이미지 → R2 마이그레이션 ===');
|
||||||
|
$this->info('Source: '.$sourceBase);
|
||||||
|
$this->info('Mode: '.($dryRun ? 'DRY-RUN' : 'LIVE'));
|
||||||
|
$this->newLine();
|
||||||
|
|
||||||
|
// 1. BENDING 아이템에서 legacy_bending_num이 있는 것 조회
|
||||||
|
$items = DB::table('items')
|
||||||
|
->where('tenant_id', $tenantId)
|
||||||
|
->where('item_category', 'BENDING')
|
||||||
|
->whereNull('deleted_at')
|
||||||
|
->get(['id', 'code', 'options']);
|
||||||
|
|
||||||
|
$this->info("BENDING 아이템: {$items->count()}건");
|
||||||
|
|
||||||
|
// legacy_bending_num → chandj imgdata 매핑
|
||||||
|
$chandjImages = DB::connection('chandj')->table('bending')
|
||||||
|
->whereNull('is_deleted')
|
||||||
|
->whereNotNull('imgdata')
|
||||||
|
->where('imgdata', '!=', '')
|
||||||
|
->pluck('imgdata', 'num');
|
||||||
|
|
||||||
|
$this->info("chandj 이미지: {$chandjImages->count()}건");
|
||||||
|
$this->newLine();
|
||||||
|
|
||||||
|
foreach ($items as $item) {
|
||||||
|
$opts = json_decode($item->options ?? '{}', true) ?: [];
|
||||||
|
$legacyNum = $opts['legacy_bending_num'] ?? null;
|
||||||
|
|
||||||
|
if (! $legacyNum || ! isset($chandjImages[$legacyNum])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 이미 파일이 연결되어 있으면 스킵
|
||||||
|
$existingFile = File::where('tenant_id', $tenantId)
|
||||||
|
->where('document_type', '1')
|
||||||
|
->where('document_id', $item->id)
|
||||||
|
->where('field_key', 'bending_diagram')
|
||||||
|
->whereNull('deleted_at')
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if ($existingFile) {
|
||||||
|
$this->skipped++;
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$imgFilename = $chandjImages[$legacyNum];
|
||||||
|
$imageUrl = "{$sourceBase}/{$imgFilename}";
|
||||||
|
|
||||||
|
if ($dryRun) {
|
||||||
|
$this->line(" ✅ {$item->code} ← {$imgFilename}");
|
||||||
|
$this->uploaded++;
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 이미지 다운로드
|
||||||
|
try {
|
||||||
|
$response = Http::withoutVerifying()->timeout(15)->get($imageUrl);
|
||||||
|
|
||||||
|
if (! $response->successful()) {
|
||||||
|
$this->warn(" ❌ {$item->code}: HTTP {$response->status()} ({$imageUrl})");
|
||||||
|
$this->failed++;
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$imageContent = $response->body();
|
||||||
|
$mimeType = $response->header('Content-Type', 'image/png');
|
||||||
|
$extension = $this->getExtension($imgFilename, $mimeType);
|
||||||
|
|
||||||
|
// R2 저장
|
||||||
|
$storedName = bin2hex(random_bytes(8)).'.'.$extension;
|
||||||
|
$year = date('Y');
|
||||||
|
$month = date('m');
|
||||||
|
$directory = sprintf('%d/items/%s/%s', $tenantId, $year, $month);
|
||||||
|
$filePath = $directory.'/'.$storedName;
|
||||||
|
|
||||||
|
Storage::disk('r2')->put($filePath, $imageContent);
|
||||||
|
|
||||||
|
// files 테이블 저장
|
||||||
|
$file = File::create([
|
||||||
|
'tenant_id' => $tenantId,
|
||||||
|
'display_name' => $imgFilename,
|
||||||
|
'stored_name' => $storedName,
|
||||||
|
'file_path' => $filePath,
|
||||||
|
'file_size' => strlen($imageContent),
|
||||||
|
'mime_type' => $mimeType,
|
||||||
|
'file_type' => 'image',
|
||||||
|
'field_key' => 'bending_diagram',
|
||||||
|
'document_id' => $item->id,
|
||||||
|
'document_type' => '1',
|
||||||
|
'is_temp' => false,
|
||||||
|
'uploaded_by' => 1,
|
||||||
|
'created_by' => 1,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->line(" ✅ {$item->code} ← {$imgFilename} → file_id={$file->id}");
|
||||||
|
$this->uploaded++;
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->error(" ❌ {$item->code}: {$e->getMessage()}");
|
||||||
|
$this->failed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->newLine();
|
||||||
|
$this->info("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
|
||||||
|
$this->info("업로드: {$this->uploaded}건 | 스킵(이미 있음): {$this->skipped}건 | 실패: {$this->failed}건");
|
||||||
|
|
||||||
|
if ($dryRun) {
|
||||||
|
$this->info('🔍 DRY-RUN 완료.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getExtension(string $filename, string $mimeType): string
|
||||||
|
{
|
||||||
|
$ext = pathinfo($filename, PATHINFO_EXTENSION);
|
||||||
|
if ($ext) {
|
||||||
|
return strtolower($ext);
|
||||||
|
}
|
||||||
|
|
||||||
|
return match ($mimeType) {
|
||||||
|
'image/jpeg' => 'jpg',
|
||||||
|
'image/png' => 'png',
|
||||||
|
'image/gif' => 'gif',
|
||||||
|
'image/webp' => 'webp',
|
||||||
|
default => 'png',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
357
app/Console/Commands/BendingImportLegacy.php
Normal file
357
app/Console/Commands/BendingImportLegacy.php
Normal file
@@ -0,0 +1,357 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use Illuminate\Console\Attributes\AsCommand;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 3단계: chandj.bending → SAM items.options 전개도(bendingData) + 속성 임포트
|
||||||
|
*
|
||||||
|
* chandj bending 265건 → SAM items (item_category=BENDING) 170건
|
||||||
|
*
|
||||||
|
* 매핑 방식:
|
||||||
|
* A) 한글 패턴 (58건): code 파싱으로 item_spec/material 매칭
|
||||||
|
* B) PREFIX-LEN (112건): PREFIX → 부품 유형 → chandj item_bending+itemName+material 매칭
|
||||||
|
*
|
||||||
|
* 실행: php artisan bending:import-legacy [--dry-run] [--tenant_id=287]
|
||||||
|
*/
|
||||||
|
#[AsCommand(name: 'bending:import-legacy', description: 'chandj 레거시 전개도(bendingData) + 속성 → SAM items.options 임포트')]
|
||||||
|
class BendingImportLegacy extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'bending:import-legacy
|
||||||
|
{--tenant_id=287 : Target tenant ID}
|
||||||
|
{--dry-run : 실제 저장 없이 미리보기}
|
||||||
|
{--force : 기존 bendingData 덮어쓰기}';
|
||||||
|
|
||||||
|
// PREFIX → chandj 매칭 조건 (item_bending + itemName 패턴 + material)
|
||||||
|
private const PREFIX_TO_CHANDJ = [
|
||||||
|
// 가이드레일 (벽면) — item_spec=120*70 (KSS01/02 기준)
|
||||||
|
'RS' => ['item_bending' => '가이드레일', 'itemName_like' => '%마감%', 'material' => 'SUS 1.2T', 'item_spec' => '120*70'],
|
||||||
|
'RE' => ['item_bending' => '가이드레일', 'itemName_like' => '%마감%', 'material' => 'EGI 1.55T', 'item_spec' => '120*70'],
|
||||||
|
'RM' => ['item_bending' => '가이드레일', 'itemName_like' => '%본체%', 'material' => 'EGI 1.55T', 'item_spec' => '120*70'],
|
||||||
|
'RC' => ['item_bending' => '가이드레일', 'itemName_like' => '%C형%', 'material' => 'EGI 1.55T', 'item_spec' => '120*70'],
|
||||||
|
'RD' => ['item_bending' => '가이드레일', 'itemName_like' => '%벽면형-D%', 'material' => 'EGI 1.55T', 'item_spec' => '120*70'],
|
||||||
|
'RT' => ['item_bending' => '가이드레일', 'itemName_like' => '%본체%', 'material' => 'EGI 1.55T', 'item_spec' => '130*75'],
|
||||||
|
// 가이드레일 (측면) — 벽면과 같은 전개도
|
||||||
|
'SS' => ['same_as' => 'RS'],
|
||||||
|
'SU' => ['same_as' => 'RS'],
|
||||||
|
'SM' => ['same_as' => 'RM'],
|
||||||
|
'SC' => ['same_as' => 'RC'],
|
||||||
|
'SD' => ['item_bending' => '가이드레일', 'itemName_like' => '%측면형-D%', 'material' => 'EGI 1.55T', 'item_spec' => '120*120'],
|
||||||
|
'ST' => ['same_as' => 'RT'],
|
||||||
|
'SE' => ['same_as' => 'RE'],
|
||||||
|
// 하단마감재
|
||||||
|
'BS' => ['item_bending' => '하단마감재', 'itemName_like' => '%하장바%', 'material_like' => '%SUS%', 'item_sep' => '스크린'],
|
||||||
|
'BE' => ['item_bending' => '하단마감재', 'itemName_like' => '%하장바%', 'material_like' => '%EGI%', 'item_sep' => '스크린'],
|
||||||
|
'TS' => ['item_bending' => '하단마감재', 'itemName_like' => '%하장바%', 'material_like' => '%SUS%', 'item_sep' => '철재'],
|
||||||
|
'LA' => ['item_bending' => 'L-BAR', 'itemName_like' => '%L-BAR%', 'material' => 'EGI 1.55T'],
|
||||||
|
'HH' => ['item_bending' => '하단마감재', 'itemName_like' => '%보강평철%', 'material_like' => '%EGI%'],
|
||||||
|
// 케이스 — spec 없이 itemName으로 구분
|
||||||
|
'CF' => ['item_bending' => '케이스', 'itemName_like' => '%전면%', 'item_sep' => '스크린'],
|
||||||
|
'CL' => ['item_bending' => '케이스', 'itemName_like' => '%린텔%', 'item_sep' => '스크린'],
|
||||||
|
'CP' => ['item_bending' => '케이스', 'itemName_like' => '%점검%', 'item_sep' => '스크린'],
|
||||||
|
'CB' => ['item_bending' => '케이스', 'itemName_like' => '%후면%', 'item_sep' => '스크린'],
|
||||||
|
// 연기차단재
|
||||||
|
'GI' => ['item_bending' => '연기차단재', 'itemName_like' => '%연기%'],
|
||||||
|
// 공용
|
||||||
|
'XX' => null, // 여러 부품이 섞여 있어 자동 매핑 불가
|
||||||
|
'YY' => null, // 별도 마감 — 자동 매핑 불가
|
||||||
|
];
|
||||||
|
|
||||||
|
private array $stats = [
|
||||||
|
'total_sam' => 0,
|
||||||
|
'matched' => 0,
|
||||||
|
'updated' => 0,
|
||||||
|
'already_has' => 0,
|
||||||
|
'no_match' => 0,
|
||||||
|
'no_bending_data' => 0,
|
||||||
|
];
|
||||||
|
|
||||||
|
private array $unmatchedItems = [];
|
||||||
|
|
||||||
|
public function handle(): int
|
||||||
|
{
|
||||||
|
$tenantId = (int) $this->option('tenant_id');
|
||||||
|
$dryRun = $this->option('dry-run');
|
||||||
|
$force = $this->option('force');
|
||||||
|
|
||||||
|
$this->info('=== 3단계: chandj 전개도 → SAM options 임포트 ===');
|
||||||
|
$this->info('Tenant: '.$tenantId.' | Mode: '.($dryRun ? 'DRY-RUN' : 'LIVE').($force ? ' (FORCE)' : ''));
|
||||||
|
$this->newLine();
|
||||||
|
|
||||||
|
// 1. chandj bending 전체 로드
|
||||||
|
$chandjRows = DB::connection('chandj')->table('bending')
|
||||||
|
->whereNull('is_deleted')
|
||||||
|
->get();
|
||||||
|
$this->info("chandj bending 활성: {$chandjRows->count()}건");
|
||||||
|
|
||||||
|
// 2. SAM BENDING items 전체 로드
|
||||||
|
$samItems = DB::table('items')
|
||||||
|
->where('tenant_id', $tenantId)
|
||||||
|
->where('item_category', 'BENDING')
|
||||||
|
->whereNull('deleted_at')
|
||||||
|
->orderBy('code')
|
||||||
|
->get(['id', 'code', 'name', 'options']);
|
||||||
|
|
||||||
|
$this->stats['total_sam'] = $samItems->count();
|
||||||
|
$this->info("SAM BENDING items: {$samItems->count()}건");
|
||||||
|
$this->newLine();
|
||||||
|
|
||||||
|
// 3. 매칭 + 임포트
|
||||||
|
foreach ($samItems as $item) {
|
||||||
|
$options = json_decode($item->options ?? '{}', true) ?: [];
|
||||||
|
|
||||||
|
// 이미 bendingData가 있으면 skip (--force 아닌 경우)
|
||||||
|
if (! empty($options['bendingData']) && ! $force) {
|
||||||
|
$this->stats['already_has']++;
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// chandj 매칭
|
||||||
|
$chandjRow = $this->findChandjMatch($item->code, $options, $chandjRows);
|
||||||
|
|
||||||
|
if (! $chandjRow) {
|
||||||
|
$this->stats['no_match']++;
|
||||||
|
$this->unmatchedItems[] = $item->code;
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// bendingData 변환
|
||||||
|
$bendingData = $this->convertBendingData($chandjRow);
|
||||||
|
|
||||||
|
if (empty($bendingData)) {
|
||||||
|
$this->stats['no_bending_data']++;
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// options 업데이트
|
||||||
|
$updates = ['bendingData' => $bendingData];
|
||||||
|
|
||||||
|
// 추가 속성 (비어있으면 채우기)
|
||||||
|
$optionalFields = [
|
||||||
|
'memo' => $chandjRow->memo,
|
||||||
|
'author' => $chandjRow->author,
|
||||||
|
'search_keyword' => $chandjRow->search_keyword,
|
||||||
|
'registration_date' => $chandjRow->registration_date,
|
||||||
|
'model_UA' => $chandjRow->model_UA,
|
||||||
|
'exit_direction' => $chandjRow->exit_direction,
|
||||||
|
'front_bottom_width' => $chandjRow->front_bottom_width,
|
||||||
|
'rail_width' => $chandjRow->rail_width,
|
||||||
|
'box_width' => $chandjRow->box_width,
|
||||||
|
'box_height' => $chandjRow->box_height,
|
||||||
|
'item_spec' => $chandjRow->item_spec,
|
||||||
|
'legacy_bending_num' => $chandjRow->num,
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($optionalFields as $key => $value) {
|
||||||
|
if (! empty($value) && empty($options[$key])) {
|
||||||
|
$updates[$key] = $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$merged = array_merge($options, $updates);
|
||||||
|
|
||||||
|
if (! $dryRun) {
|
||||||
|
DB::table('items')->where('id', $item->id)->update([
|
||||||
|
'options' => json_encode($merged, JSON_UNESCAPED_UNICODE),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->stats['matched']++;
|
||||||
|
$this->stats['updated']++;
|
||||||
|
$colCount = count($bendingData);
|
||||||
|
$this->line(" ✅ {$item->code} ← chandj#{$chandjRow->num} (전개도 {$colCount}열, +".implode(',', array_keys($updates)).')');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->showStats($dryRun);
|
||||||
|
|
||||||
|
return self::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SAM item code → chandj bending 매칭
|
||||||
|
*/
|
||||||
|
private function findChandjMatch(string $code, array $options, $chandjRows): ?object
|
||||||
|
{
|
||||||
|
// A) 한글 패턴 — code에서 속성 추출하여 매칭
|
||||||
|
if (! preg_match('/^BD-[A-Z]{2}-\d{2}$/', $code)) {
|
||||||
|
return $this->matchKoreanPattern($code, $chandjRows);
|
||||||
|
}
|
||||||
|
|
||||||
|
// B) PREFIX-LEN — PREFIX로 chandj 조건 결정
|
||||||
|
preg_match('/^BD-([A-Z]{2})-\d{2}$/', $code, $m);
|
||||||
|
$prefix = $m[1];
|
||||||
|
|
||||||
|
$mapping = self::PREFIX_TO_CHANDJ[$prefix] ?? null;
|
||||||
|
if (! $mapping) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// same_as 참조
|
||||||
|
if (isset($mapping['same_as'])) {
|
||||||
|
$mapping = self::PREFIX_TO_CHANDJ[$mapping['same_as']] ?? null;
|
||||||
|
if (! $mapping) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->queryChangj($chandjRows, $mapping);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 한글 패턴 매칭
|
||||||
|
*/
|
||||||
|
private function matchKoreanPattern(string $code, $chandjRows): ?object
|
||||||
|
{
|
||||||
|
// BD-가이드레일-KSS01-SUS-120*70
|
||||||
|
if (preg_match('/^BD-가이드레일-(\w+)-(\w+)-(.+)$/', $code, $m)) {
|
||||||
|
$material = str_contains($m[2], 'SUS') ? 'SUS 1.2T' : 'EGI 1.55T';
|
||||||
|
|
||||||
|
return $this->queryChangj($chandjRows, [
|
||||||
|
'item_bending' => '가이드레일',
|
||||||
|
'material' => $material,
|
||||||
|
'item_spec' => $m[3],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// BD-하단마감재-KSS01-SUS-60*40
|
||||||
|
if (preg_match('/^BD-하단마감재-(\w+)-(\w+)-(.+)$/', $code, $m)) {
|
||||||
|
$material = str_contains($m[2], 'SUS') ? 'SUS' : 'EGI';
|
||||||
|
|
||||||
|
return $this->queryChangj($chandjRows, [
|
||||||
|
'item_bending' => '하단마감재',
|
||||||
|
'material_like' => "%{$material}%",
|
||||||
|
'item_spec_like' => "%{$m[3]}%",
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// BD-케이스-650*550 → chandj에서 itemName에 "650*550" 포함된 전면판 매칭
|
||||||
|
if (preg_match('/^BD-케이스-(\d+)\*(\d+)$/', $code, $m)) {
|
||||||
|
$spec = $m[1].'*'.$m[2];
|
||||||
|
|
||||||
|
return $chandjRows->first(function ($r) use ($spec) {
|
||||||
|
return (str_contains($r->itemName, $spec) || $r->item_spec === $spec)
|
||||||
|
&& str_contains($r->itemName, '전면');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// BD-마구리-655*505
|
||||||
|
if (preg_match('/^BD-마구리-(.+)$/', $code, $m)) {
|
||||||
|
return $chandjRows->first(fn ($r) => $r->item_bending === '마구리' && $r->item_spec === $m[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// BD-L-BAR-KSS01-17*60
|
||||||
|
if (preg_match('/^BD-L-BAR-\w+-(.+)$/', $code, $m)) {
|
||||||
|
return $chandjRows->first(fn ($r) => $r->item_bending === 'L-BAR' && $r->item_spec === $m[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// BD-보강평철-50
|
||||||
|
if (preg_match('/^BD-보강평철-(.+)$/', $code, $m)) {
|
||||||
|
return $chandjRows->first(fn ($r) => str_contains($r->itemName, '보강평철') && $r->item_spec === $m[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* chandj 컬렉션에서 조건으로 검색
|
||||||
|
*/
|
||||||
|
private function queryChangj($rows, array $cond): ?object
|
||||||
|
{
|
||||||
|
return $rows->first(function ($r) use ($cond) {
|
||||||
|
if (isset($cond['item_sep']) && $r->item_sep !== $cond['item_sep']) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (isset($cond['item_bending']) && $r->item_bending !== $cond['item_bending']) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (isset($cond['material']) && $r->material !== $cond['material']) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (isset($cond['material_like']) && ! str_contains($r->material, str_replace('%', '', $cond['material_like']))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (isset($cond['itemName_like']) && ! str_contains($r->itemName, str_replace('%', '', $cond['itemName_like']))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (isset($cond['item_spec']) && $r->item_spec !== $cond['item_spec']) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (isset($cond['item_spec_like']) && ! str_contains($r->item_spec ?? '', str_replace('%', '', $cond['item_spec_like']))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* chandj bending row → bendingData JSON 배열 변환
|
||||||
|
*
|
||||||
|
* 레거시: inputList=["10","11","110"], bendingrateList=["","-1",""], sumList=["10","21","131"], colorList=[false,false,true], AList=[false,false,false]
|
||||||
|
* SAM: [{"no":1,"input":10,"rate":"","sum":10,"color":false,"aAngle":false}, ...]
|
||||||
|
*/
|
||||||
|
private function convertBendingData(object $row): array
|
||||||
|
{
|
||||||
|
$inputs = json_decode($row->inputList ?? '[]', true) ?: [];
|
||||||
|
$rates = json_decode($row->bendingrateList ?? '[]', true) ?: [];
|
||||||
|
$sums = json_decode($row->sumList ?? '[]', true) ?: [];
|
||||||
|
$colors = json_decode($row->colorList ?? '[]', true) ?: [];
|
||||||
|
$angles = json_decode($row->AList ?? '[]', true) ?: [];
|
||||||
|
|
||||||
|
if (empty($inputs)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = [];
|
||||||
|
$count = count($inputs);
|
||||||
|
|
||||||
|
for ($i = 0; $i < $count; $i++) {
|
||||||
|
$data[] = [
|
||||||
|
'no' => $i + 1,
|
||||||
|
'input' => (float) ($inputs[$i] ?? 0),
|
||||||
|
'rate' => (string) ($rates[$i] ?? ''),
|
||||||
|
'sum' => (float) ($sums[$i] ?? 0),
|
||||||
|
'color' => (bool) ($colors[$i] ?? false),
|
||||||
|
'aAngle' => (bool) ($angles[$i] ?? false),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function showStats(bool $dryRun): void
|
||||||
|
{
|
||||||
|
$this->newLine();
|
||||||
|
$this->info('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||||||
|
$this->info('📊 결과'.($dryRun ? ' (DRY-RUN)' : ''));
|
||||||
|
$this->info('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||||||
|
$this->info(" SAM BENDING 전체: {$this->stats['total_sam']}건");
|
||||||
|
$this->info(" 매칭 성공 (업데이트): {$this->stats['updated']}건");
|
||||||
|
$this->info(" 이미 bendingData 있음 (skip): {$this->stats['already_has']}건");
|
||||||
|
$this->info(" 매칭 실패: {$this->stats['no_match']}건");
|
||||||
|
if ($this->stats['no_bending_data'] > 0) {
|
||||||
|
$this->warn(" 전개도 데이터 없음: {$this->stats['no_bending_data']}건");
|
||||||
|
}
|
||||||
|
$this->info('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||||||
|
|
||||||
|
if (! empty($this->unmatchedItems)) {
|
||||||
|
$this->newLine();
|
||||||
|
$this->warn('⚠️ 매칭 실패 항목:');
|
||||||
|
foreach ($this->unmatchedItems as $code) {
|
||||||
|
$this->line(" - {$code}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($dryRun) {
|
||||||
|
$this->newLine();
|
||||||
|
$this->info('🔍 DRY-RUN 완료. --dry-run 제거하면 실제 반영됩니다.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
192
app/Console/Commands/BendingModelImportAssemblyImages.php
Normal file
192
app/Console/Commands/BendingModelImportAssemblyImages.php
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use App\Models\Commons\File;
|
||||||
|
use Illuminate\Console\Attributes\AsCommand;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Http;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 레거시 guiderail.json 결합형태 이미지 → SAM 모델 연결
|
||||||
|
*/
|
||||||
|
#[AsCommand(name: 'bending-model:import-assembly-images', description: '결합형태 이미지 → R2 마이그레이션')]
|
||||||
|
class BendingModelImportAssemblyImages extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'bending-model:import-assembly-images
|
||||||
|
{--tenant_id=287 : Target tenant ID}
|
||||||
|
{--dry-run : 실제 저장 없이 미리보기}
|
||||||
|
{--source=https://5130.codebridge-x.com : 소스 URL}';
|
||||||
|
|
||||||
|
public function handle(): int
|
||||||
|
{
|
||||||
|
$tenantId = (int) $this->option('tenant_id');
|
||||||
|
$dryRun = $this->option('dry-run');
|
||||||
|
$sourceBase = rtrim($this->option('source'), '/');
|
||||||
|
|
||||||
|
$this->info('=== 결합형태 이미지 → R2 마이그레이션 ===');
|
||||||
|
|
||||||
|
// 3개 JSON 파일 순차 처리
|
||||||
|
$jsonConfigs = [
|
||||||
|
['file' => 'guiderail/guiderail.json', 'category' => 'GUIDERAIL_MODEL', 'imageBase' => ''],
|
||||||
|
['file' => 'shutterbox/shutterbox.json', 'category' => 'SHUTTERBOX_MODEL', 'imageBase' => ''],
|
||||||
|
['file' => 'bottombar/bottombar.json', 'category' => 'BOTTOMBAR_MODEL', 'imageBase' => ''],
|
||||||
|
];
|
||||||
|
|
||||||
|
$uploaded = 0;
|
||||||
|
$skipped = 0;
|
||||||
|
$failed = 0;
|
||||||
|
|
||||||
|
foreach ($jsonConfigs as $jsonConfig) {
|
||||||
|
$jsonPath = base_path('../5130/' . $jsonConfig['file']);
|
||||||
|
if (! file_exists($jsonPath)) {
|
||||||
|
$resp = Http::withoutVerifying()->get("{$sourceBase}/{$jsonConfig['file']}");
|
||||||
|
$assemblyData = $resp->successful() ? $resp->json() : [];
|
||||||
|
} else {
|
||||||
|
$assemblyData = json_decode(file_get_contents($jsonPath), true) ?: [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->info("--- {$jsonConfig['category']} ({$jsonConfig['file']}): " . count($assemblyData) . '건 ---');
|
||||||
|
|
||||||
|
foreach ($assemblyData as $entry) {
|
||||||
|
$imagePath = $entry['image'] ?? '';
|
||||||
|
if (! $imagePath) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// SAM 코드 생성 (카테고리별)
|
||||||
|
$code = $this->buildCode($entry, $jsonConfig['category']);
|
||||||
|
if (! $code) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$samItem = DB::table('items')
|
||||||
|
->where('tenant_id', $tenantId)
|
||||||
|
->where('code', $code)
|
||||||
|
->where('item_category', $jsonConfig['category'])
|
||||||
|
->whereNull('deleted_at')
|
||||||
|
->first(['id', 'code', 'options']);
|
||||||
|
|
||||||
|
if (! $samItem) {
|
||||||
|
$this->warn(" ⚠️ {$code}: SAM 모델 없음");
|
||||||
|
$failed++;
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 이미 이미지 있으면 스킵
|
||||||
|
$existing = File::where('tenant_id', $tenantId)
|
||||||
|
->where('document_id', $samItem->id)
|
||||||
|
->where('field_key', 'assembly_image')
|
||||||
|
->whereNull('deleted_at')
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if ($existing) {
|
||||||
|
$skipped++;
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$imageUrl = "{$sourceBase}{$imagePath}";
|
||||||
|
|
||||||
|
if ($dryRun) {
|
||||||
|
$this->line(" ✅ {$code} ← {$imagePath}");
|
||||||
|
$uploaded++;
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$response = Http::withoutVerifying()->timeout(15)->get($imageUrl);
|
||||||
|
|
||||||
|
if (! $response->successful()) {
|
||||||
|
$this->warn(" ❌ {$code}: HTTP {$response->status()}");
|
||||||
|
$failed++;
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$content = $response->body();
|
||||||
|
$ext = pathinfo($imagePath, PATHINFO_EXTENSION) ?: 'png';
|
||||||
|
$storedName = bin2hex(random_bytes(8)).'.'.strtolower($ext);
|
||||||
|
$directory = sprintf('%d/items/%s/%s', $tenantId, date('Y'), date('m'));
|
||||||
|
$filePath = $directory.'/'.$storedName;
|
||||||
|
|
||||||
|
Storage::disk('r2')->put($filePath, $content);
|
||||||
|
|
||||||
|
$file = File::create([
|
||||||
|
'tenant_id' => $tenantId,
|
||||||
|
'display_name' => basename($imagePath),
|
||||||
|
'stored_name' => $storedName,
|
||||||
|
'file_path' => $filePath,
|
||||||
|
'file_size' => strlen($content),
|
||||||
|
'mime_type' => $response->header('Content-Type', 'image/png'),
|
||||||
|
'file_type' => 'image',
|
||||||
|
'field_key' => 'assembly_image',
|
||||||
|
'document_id' => $samItem->id,
|
||||||
|
'document_type' => '1',
|
||||||
|
'is_temp' => false,
|
||||||
|
'uploaded_by' => 1,
|
||||||
|
'created_by' => 1,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->line(" ✅ {$code} ← {$imagePath} → file_id={$file->id}");
|
||||||
|
$uploaded++;
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->error(" ❌ {$code}: {$e->getMessage()}");
|
||||||
|
$failed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // end foreach jsonConfigs
|
||||||
|
|
||||||
|
$this->newLine();
|
||||||
|
$this->info("업로드: {$uploaded}건 | 스킵: {$skipped}건 | 실패: {$failed}건");
|
||||||
|
|
||||||
|
return self::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildCode(array $entry, string $category): ?string
|
||||||
|
{
|
||||||
|
if ($category === 'GUIDERAIL_MODEL') {
|
||||||
|
$modelName = $entry['model_name'] ?? '';
|
||||||
|
$checkType = $entry['check_type'] ?? '';
|
||||||
|
$finishType = $entry['finishing_type'] ?? '';
|
||||||
|
if (! $modelName) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$finish = str_contains($finishType, 'SUS') ? 'SUS' : 'EGI';
|
||||||
|
|
||||||
|
return "GR-{$modelName}-{$checkType}-{$finish}";
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($category === 'SHUTTERBOX_MODEL') {
|
||||||
|
$w = $entry['box_width'] ?? '';
|
||||||
|
$h = $entry['box_height'] ?? '';
|
||||||
|
$exit = $entry['exit_direction'] ?? '';
|
||||||
|
$exitShort = match ($exit) {
|
||||||
|
'양면 점검구' => '양면',
|
||||||
|
'밑면 점검구' => '밑면',
|
||||||
|
'후면 점검구' => '후면',
|
||||||
|
default => $exit,
|
||||||
|
};
|
||||||
|
|
||||||
|
return "SB-{$w}*{$h}-{$exitShort}";
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($category === 'BOTTOMBAR_MODEL') {
|
||||||
|
$modelName = $entry['model_name'] ?? '';
|
||||||
|
$finishType = $entry['finishing_type'] ?? '';
|
||||||
|
if (! $modelName) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$finish = str_contains($finishType, 'SUS') ? 'SUS' : 'EGI';
|
||||||
|
|
||||||
|
return "BB-{$modelName}-{$finish}";
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
160
app/Console/Commands/BendingModelImportImages.php
Normal file
160
app/Console/Commands/BendingModelImportImages.php
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use App\Models\Commons\File;
|
||||||
|
use Illuminate\Console\Attributes\AsCommand;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Http;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 가이드레일/케이스/하단마감재 모델의 부품별 이미지 임포트
|
||||||
|
*
|
||||||
|
* chandj guiderail/shutterbox/bottombar components의 imgdata →
|
||||||
|
* 5130.codebridge-x.com에서 다운로드 → R2 업로드 → components에 file_id 추가
|
||||||
|
*/
|
||||||
|
#[AsCommand(name: 'bending-model:import-images', description: '절곡품 모델 부품별 이미지 → R2 마이그레이션')]
|
||||||
|
class BendingModelImportImages extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'bending-model:import-images
|
||||||
|
{--tenant_id=287 : Target tenant ID}
|
||||||
|
{--dry-run : 실제 저장 없이 미리보기}
|
||||||
|
{--source=https://5130.codebridge-x.com/bending/img : 이미지 소스 URL}';
|
||||||
|
|
||||||
|
private int $uploaded = 0;
|
||||||
|
|
||||||
|
private int $skipped = 0;
|
||||||
|
|
||||||
|
private int $failed = 0;
|
||||||
|
|
||||||
|
public function handle(): int
|
||||||
|
{
|
||||||
|
$tenantId = (int) $this->option('tenant_id');
|
||||||
|
$dryRun = $this->option('dry-run');
|
||||||
|
$sourceBase = rtrim($this->option('source'), '/');
|
||||||
|
|
||||||
|
$this->info('=== 절곡품 모델 부품 이미지 → R2 마이그레이션 ===');
|
||||||
|
$this->info('Mode: '.($dryRun ? 'DRY-RUN' : 'LIVE'));
|
||||||
|
$this->newLine();
|
||||||
|
|
||||||
|
// chandj에서 원본 imgdata 조회
|
||||||
|
$chandjTables = [
|
||||||
|
'GUIDERAIL_MODEL' => 'guiderail',
|
||||||
|
'SHUTTERBOX_MODEL' => 'shutterbox',
|
||||||
|
'BOTTOMBAR_MODEL' => 'bottombar',
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($chandjTables as $category => $table) {
|
||||||
|
$this->info("--- {$category} ({$table}) ---");
|
||||||
|
|
||||||
|
$chandjRows = DB::connection('chandj')->table($table)->whereNull('is_deleted')->get();
|
||||||
|
$samItems = DB::table('items')->where('tenant_id', $tenantId)
|
||||||
|
->where('item_category', $category)->whereNull('deleted_at')
|
||||||
|
->get(['id', 'code', 'options']);
|
||||||
|
|
||||||
|
// legacy_num → chandj row 매핑
|
||||||
|
$chandjMap = [];
|
||||||
|
foreach ($chandjRows as $row) {
|
||||||
|
$chandjMap[$row->num] = $row;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($samItems as $samItem) {
|
||||||
|
$opts = json_decode($samItem->options, true) ?? [];
|
||||||
|
$legacyNum = $opts['legacy_num'] ?? $opts['legacy_guiderail_num'] ?? null;
|
||||||
|
|
||||||
|
if (! $legacyNum || ! isset($chandjMap[$legacyNum])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$chandjRow = $chandjMap[$legacyNum];
|
||||||
|
$chandjComps = json_decode($chandjRow->bending_components ?? '[]', true) ?: [];
|
||||||
|
$components = $opts['components'] ?? [];
|
||||||
|
$updated = false;
|
||||||
|
|
||||||
|
foreach ($components as $idx => &$comp) {
|
||||||
|
// chandj component에서 imgdata 찾기
|
||||||
|
$chandjComp = $chandjComps[$idx] ?? null;
|
||||||
|
$imgdata = $chandjComp['imgdata'] ?? null;
|
||||||
|
|
||||||
|
if (! $imgdata || ! empty($comp['image_file_id'])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$imageUrl = "{$sourceBase}/{$imgdata}";
|
||||||
|
|
||||||
|
if ($dryRun) {
|
||||||
|
$this->line(" ✅ {$samItem->code} #{$idx} ← {$imgdata}");
|
||||||
|
$this->uploaded++;
|
||||||
|
$updated = true;
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$response = Http::withoutVerifying()->timeout(15)->get($imageUrl);
|
||||||
|
|
||||||
|
if (! $response->successful()) {
|
||||||
|
$this->warn(" ❌ {$samItem->code} #{$idx}: HTTP {$response->status()}");
|
||||||
|
$this->failed++;
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$imageContent = $response->body();
|
||||||
|
$extension = pathinfo($imgdata, PATHINFO_EXTENSION) ?: 'png';
|
||||||
|
$storedName = bin2hex(random_bytes(8)).'.'.strtolower($extension);
|
||||||
|
$directory = sprintf('%d/items/%s/%s', $tenantId, date('Y'), date('m'));
|
||||||
|
$filePath = $directory.'/'.$storedName;
|
||||||
|
|
||||||
|
Storage::disk('r2')->put($filePath, $imageContent);
|
||||||
|
|
||||||
|
$file = File::create([
|
||||||
|
'tenant_id' => $tenantId,
|
||||||
|
'display_name' => $imgdata,
|
||||||
|
'stored_name' => $storedName,
|
||||||
|
'file_path' => $filePath,
|
||||||
|
'file_size' => strlen($imageContent),
|
||||||
|
'mime_type' => $response->header('Content-Type', 'image/png'),
|
||||||
|
'file_type' => 'image',
|
||||||
|
'field_key' => 'bending_component_image',
|
||||||
|
'document_id' => $samItem->id,
|
||||||
|
'document_type' => '1',
|
||||||
|
'is_temp' => false,
|
||||||
|
'uploaded_by' => 1,
|
||||||
|
'created_by' => 1,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$comp['image_file_id'] = $file->id;
|
||||||
|
$comp['imgdata'] = $imgdata;
|
||||||
|
$updated = true;
|
||||||
|
$this->uploaded++;
|
||||||
|
$this->line(" ✅ {$samItem->code} #{$idx} {$comp['itemName']} ← {$imgdata} → file_id={$file->id}");
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->error(" ❌ {$samItem->code} #{$idx}: {$e->getMessage()}");
|
||||||
|
$this->failed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
unset($comp);
|
||||||
|
|
||||||
|
// components 업데이트
|
||||||
|
if ($updated && ! $dryRun) {
|
||||||
|
$opts['components'] = $components;
|
||||||
|
DB::table('items')->where('id', $samItem->id)->update([
|
||||||
|
'options' => json_encode($opts, JSON_UNESCAPED_UNICODE),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->newLine();
|
||||||
|
$this->info("업로드: {$this->uploaded}건 | 스킵: {$this->skipped}건 | 실패: {$this->failed}건");
|
||||||
|
if ($dryRun) {
|
||||||
|
$this->info('🔍 DRY-RUN 완료.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::SUCCESS;
|
||||||
|
}
|
||||||
|
}
|
||||||
200
app/Console/Commands/BendingProductImportLegacy.php
Normal file
200
app/Console/Commands/BendingProductImportLegacy.php
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use Illuminate\Console\Attributes\AsCommand;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* chandj shutterbox(케이스) + bottombar(하단마감재) → SAM items 임포트
|
||||||
|
*/
|
||||||
|
#[AsCommand(name: 'bending-product:import-legacy', description: 'chandj 케이스/하단마감재 모델 → SAM items 임포트')]
|
||||||
|
class BendingProductImportLegacy extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'bending-product:import-legacy
|
||||||
|
{--tenant_id=287 : Target tenant ID}
|
||||||
|
{--dry-run : 실제 저장 없이 미리보기}';
|
||||||
|
|
||||||
|
public function handle(): int
|
||||||
|
{
|
||||||
|
$tenantId = (int) $this->option('tenant_id');
|
||||||
|
$dryRun = $this->option('dry-run');
|
||||||
|
|
||||||
|
$this->info('=== 케이스/하단마감재 모델 → SAM 임포트 ===');
|
||||||
|
$this->info('Mode: '.($dryRun ? 'DRY-RUN' : 'LIVE'));
|
||||||
|
$this->newLine();
|
||||||
|
|
||||||
|
// 케이스 (shutterbox)
|
||||||
|
$this->info('--- 케이스 (shutterbox) ---');
|
||||||
|
$cases = DB::connection('chandj')->table('shutterbox')->whereNull('is_deleted')->get();
|
||||||
|
$this->info("chandj shutterbox: {$cases->count()}건");
|
||||||
|
$caseCreated = $this->importItems($cases, 'SHUTTERBOX_MODEL', $tenantId, $dryRun);
|
||||||
|
|
||||||
|
$this->newLine();
|
||||||
|
|
||||||
|
// 하단마감재 (bottombar)
|
||||||
|
$this->info('--- 하단마감재 (bottombar) ---');
|
||||||
|
$bars = DB::connection('chandj')->table('bottombar')->whereNull('is_deleted')->get();
|
||||||
|
$this->info("chandj bottombar: {$bars->count()}건");
|
||||||
|
$barCreated = $this->importItems($bars, 'BOTTOMBAR_MODEL', $tenantId, $dryRun);
|
||||||
|
|
||||||
|
$this->newLine();
|
||||||
|
$this->info("결과: 케이스 {$caseCreated}건 + 하단마감재 {$barCreated}건");
|
||||||
|
if ($dryRun) {
|
||||||
|
$this->info('🔍 DRY-RUN 완료.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function importItems($rows, string $category, int $tenantId, bool $dryRun): int
|
||||||
|
{
|
||||||
|
$created = 0;
|
||||||
|
$skipped = 0;
|
||||||
|
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
$code = $this->buildCode($row, $category);
|
||||||
|
$name = $this->buildName($row, $category);
|
||||||
|
|
||||||
|
$existing = DB::table('items')
|
||||||
|
->where('tenant_id', $tenantId)
|
||||||
|
->where('code', $code)
|
||||||
|
->whereNull('deleted_at')
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if ($existing) {
|
||||||
|
$skipped++;
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$components = $this->convertComponents(json_decode($row->bending_components ?? '[]', true) ?: []);
|
||||||
|
$materialSummary = json_decode($row->material_summary ?? '{}', true) ?: [];
|
||||||
|
|
||||||
|
$options = $this->buildOptions($row, $category, $components, $materialSummary);
|
||||||
|
|
||||||
|
if (! $dryRun) {
|
||||||
|
DB::table('items')->insert([
|
||||||
|
'tenant_id' => $tenantId,
|
||||||
|
'code' => $code,
|
||||||
|
'name' => $name,
|
||||||
|
'item_type' => 'FG',
|
||||||
|
'item_category' => $category,
|
||||||
|
'unit' => 'SET',
|
||||||
|
'options' => json_encode($options, JSON_UNESCAPED_UNICODE),
|
||||||
|
'is_active' => true,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$created++;
|
||||||
|
$this->line(" ✅ {$code} ({$name}) — 부품 ".count($components).'개');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->info(" 생성: {$created}건 | 스킵: {$skipped}건");
|
||||||
|
|
||||||
|
return $created;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildCode(object $row, string $category): string
|
||||||
|
{
|
||||||
|
if ($category === 'SHUTTERBOX_MODEL') {
|
||||||
|
$size = ($row->box_width ?? '').
|
||||||
|
'*'.($row->box_height ?? '');
|
||||||
|
$exit = match ($row->exit_direction ?? '') {
|
||||||
|
'양면 점검구' => '양면',
|
||||||
|
'밑면 점검구' => '밑면',
|
||||||
|
'후면 점검구' => '후면',
|
||||||
|
default => $row->exit_direction ?? '',
|
||||||
|
};
|
||||||
|
|
||||||
|
return "SB-{$size}-{$exit}";
|
||||||
|
}
|
||||||
|
|
||||||
|
// BOTTOMBAR_MODEL
|
||||||
|
$model = $row->model_name ?? 'UNKNOWN';
|
||||||
|
$finish = str_contains($row->finishing_type ?? '', 'SUS') ? 'SUS' : 'EGI';
|
||||||
|
|
||||||
|
return "BB-{$model}-{$finish}";
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildName(object $row, string $category): string
|
||||||
|
{
|
||||||
|
if ($category === 'SHUTTERBOX_MODEL') {
|
||||||
|
return "케이스 {$row->box_width}*{$row->box_height} {$row->exit_direction}";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "하단마감재 {$row->model_name} {$row->firstitem}";
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildOptions(object $row, string $category, array $components, array $materialSummary): array
|
||||||
|
{
|
||||||
|
$base = [
|
||||||
|
'author' => $row->author ?? null,
|
||||||
|
'registration_date' => $row->registration_date ?? null,
|
||||||
|
'search_keyword' => $row->search_keyword ?? null,
|
||||||
|
'memo' => $row->remark ?? null,
|
||||||
|
'components' => $components,
|
||||||
|
'material_summary' => $materialSummary,
|
||||||
|
'source' => 'chandj_'.(strtolower($category)),
|
||||||
|
'legacy_num' => $row->num,
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($category === 'SHUTTERBOX_MODEL') {
|
||||||
|
return array_merge($base, [
|
||||||
|
'box_width' => (int) ($row->box_width ?? 0),
|
||||||
|
'box_height' => (int) ($row->box_height ?? 0),
|
||||||
|
'exit_direction' => $row->exit_direction ?? null,
|
||||||
|
'front_bottom_width' => $row->front_bottom_width ?? null,
|
||||||
|
'rail_width' => $row->rail_width ?? null,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// BOTTOMBAR_MODEL
|
||||||
|
return array_merge($base, [
|
||||||
|
'model_name' => $row->model_name ?? null,
|
||||||
|
'item_sep' => $row->firstitem ?? null,
|
||||||
|
'model_UA' => $row->model_UA ?? null,
|
||||||
|
'finishing_type' => $row->finishing_type ?? null,
|
||||||
|
'bar_width' => $row->bar_width ?? null,
|
||||||
|
'bar_height' => $row->bar_height ?? null,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function convertComponents(array $legacyComps): array
|
||||||
|
{
|
||||||
|
return array_map(function ($c, $idx) {
|
||||||
|
$inputs = $c['inputList'] ?? [];
|
||||||
|
$rates = $c['bendingrateList'] ?? [];
|
||||||
|
$sums = $c['sumList'] ?? [];
|
||||||
|
$colors = $c['colorList'] ?? [];
|
||||||
|
$angles = $c['AList'] ?? [];
|
||||||
|
|
||||||
|
$bendingData = [];
|
||||||
|
for ($i = 0; $i < count($inputs); $i++) {
|
||||||
|
$bendingData[] = [
|
||||||
|
'no' => $i + 1,
|
||||||
|
'input' => (float) ($inputs[$i] ?? 0),
|
||||||
|
'rate' => (string) ($rates[$i] ?? ''),
|
||||||
|
'sum' => (float) ($sums[$i] ?? 0),
|
||||||
|
'color' => (bool) ($colors[$i] ?? false),
|
||||||
|
'aAngle' => (bool) ($angles[$i] ?? false),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$lastSum = ! empty($sums) ? (float) end($sums) : ($c['widthsum'] ?? 0);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'orderNumber' => $idx + 1,
|
||||||
|
'itemName' => $c['itemName'] ?? '',
|
||||||
|
'material' => $c['material'] ?? '',
|
||||||
|
'quantity' => (int) ($c['quantity'] ?? 1),
|
||||||
|
'width_sum' => (float) $lastSum,
|
||||||
|
'bendingData' => $bendingData,
|
||||||
|
'legacy_bending_num' => $c['source_num'] ?? $c['num'] ?? null,
|
||||||
|
];
|
||||||
|
}, $legacyComps, array_keys($legacyComps));
|
||||||
|
}
|
||||||
|
}
|
||||||
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
|
||||||
|
|||||||
136
app/Console/Commands/GuiderailImportLegacy.php
Normal file
136
app/Console/Commands/GuiderailImportLegacy.php
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use Illuminate\Console\Attributes\AsCommand;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* chandj.guiderail → SAM items (item_category=GUIDERAIL_MODEL) 임포트
|
||||||
|
*/
|
||||||
|
#[AsCommand(name: 'guiderail:import-legacy', description: 'chandj 가이드레일 모델 → SAM items 임포트')]
|
||||||
|
class GuiderailImportLegacy extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'guiderail:import-legacy
|
||||||
|
{--tenant_id=287 : Target tenant ID}
|
||||||
|
{--dry-run : 실제 저장 없이 미리보기}';
|
||||||
|
|
||||||
|
public function handle(): int
|
||||||
|
{
|
||||||
|
$tenantId = (int) $this->option('tenant_id');
|
||||||
|
$dryRun = $this->option('dry-run');
|
||||||
|
|
||||||
|
$this->info('=== chandj guiderail → SAM 임포트 ===');
|
||||||
|
$this->info('Mode: '.($dryRun ? 'DRY-RUN' : 'LIVE'));
|
||||||
|
|
||||||
|
$rows = DB::connection('chandj')->table('guiderail')->whereNull('is_deleted')->get();
|
||||||
|
$this->info("chandj guiderail: {$rows->count()}건");
|
||||||
|
|
||||||
|
$created = 0;
|
||||||
|
$skipped = 0;
|
||||||
|
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
$finish = str_contains($row->finishing_type ?? '', 'SUS') ? 'SUS' : 'EGI';
|
||||||
|
$code = 'GR-'.($row->model_name ?? 'UNKNOWN').'-'.($row->check_type ?? '').'-'.$finish;
|
||||||
|
$name = implode(' ', array_filter([$row->model_name, $row->check_type, $row->finishing_type]));
|
||||||
|
|
||||||
|
// 중복 확인
|
||||||
|
$existing = DB::table('items')
|
||||||
|
->where('tenant_id', $tenantId)
|
||||||
|
->where('code', $code)
|
||||||
|
->whereNull('deleted_at')
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if ($existing) {
|
||||||
|
$skipped++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// components 변환
|
||||||
|
$legacyComps = json_decode($row->bending_components ?? '[]', true) ?: [];
|
||||||
|
$components = array_map(fn ($c) => $this->convertComponent($c), $legacyComps);
|
||||||
|
|
||||||
|
$materialSummary = json_decode($row->material_summary ?? '{}', true) ?: [];
|
||||||
|
|
||||||
|
$options = [
|
||||||
|
'model_name' => $row->model_name,
|
||||||
|
'check_type' => $row->check_type,
|
||||||
|
'rail_width' => (int) $row->rail_width,
|
||||||
|
'rail_length' => (int) $row->rail_length,
|
||||||
|
'finishing_type' => $row->finishing_type,
|
||||||
|
'item_sep' => $row->firstitem,
|
||||||
|
'model_UA' => $row->model_UA,
|
||||||
|
'search_keyword' => $row->search_keyword,
|
||||||
|
'author' => $row->author,
|
||||||
|
'registration_date' => $row->registration_date,
|
||||||
|
'memo' => $row->remark,
|
||||||
|
'components' => $components,
|
||||||
|
'material_summary' => $materialSummary,
|
||||||
|
'source' => 'chandj_guiderail',
|
||||||
|
'legacy_guiderail_num' => $row->num,
|
||||||
|
];
|
||||||
|
|
||||||
|
if (! $dryRun) {
|
||||||
|
DB::table('items')->insert([
|
||||||
|
'tenant_id' => $tenantId,
|
||||||
|
'code' => $code,
|
||||||
|
'name' => $name,
|
||||||
|
'item_type' => 'FG',
|
||||||
|
'item_category' => 'GUIDERAIL_MODEL',
|
||||||
|
'unit' => 'SET',
|
||||||
|
'options' => json_encode($options, JSON_UNESCAPED_UNICODE),
|
||||||
|
'is_active' => true,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$created++;
|
||||||
|
$this->line(" ✅ {$code} ({$name}) — {$row->firstitem}/{$row->model_UA} — 부품 ".count($components).'개');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->newLine();
|
||||||
|
$this->info("생성: {$created}건 | 스킵(중복): {$skipped}건");
|
||||||
|
|
||||||
|
if ($dryRun) {
|
||||||
|
$this->info('🔍 DRY-RUN 완료.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function convertComponent(array $c): array
|
||||||
|
{
|
||||||
|
$inputs = $c['inputList'] ?? [];
|
||||||
|
$rates = $c['bendingrateList'] ?? [];
|
||||||
|
$sums = $c['sumList'] ?? [];
|
||||||
|
$colors = $c['colorList'] ?? [];
|
||||||
|
$angles = $c['AList'] ?? [];
|
||||||
|
|
||||||
|
// bendingData 형식으로 변환
|
||||||
|
$bendingData = [];
|
||||||
|
for ($i = 0; $i < count($inputs); $i++) {
|
||||||
|
$bendingData[] = [
|
||||||
|
'no' => $i + 1,
|
||||||
|
'input' => (float) ($inputs[$i] ?? 0),
|
||||||
|
'rate' => (string) ($rates[$i] ?? ''),
|
||||||
|
'sum' => (float) ($sums[$i] ?? 0),
|
||||||
|
'color' => (bool) ($colors[$i] ?? false),
|
||||||
|
'aAngle' => (bool) ($angles[$i] ?? false),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$lastSum = ! empty($sums) ? (float) end($sums) : 0;
|
||||||
|
|
||||||
|
return [
|
||||||
|
'orderNumber' => $c['orderNumber'] ?? null,
|
||||||
|
'itemName' => $c['itemName'] ?? '',
|
||||||
|
'material' => $c['material'] ?? '',
|
||||||
|
'quantity' => (int) ($c['quantity'] ?? 1),
|
||||||
|
'width_sum' => $lastSum,
|
||||||
|
'bendingData' => $bendingData,
|
||||||
|
'legacy_bending_num' => $c['num'] ?? null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
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');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -95,7 +95,7 @@ public function render($request, Throwable $exception)
|
|||||||
if ($exception instanceof BadRequestHttpException) {
|
if ($exception instanceof BadRequestHttpException) {
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'success' => false,
|
'success' => false,
|
||||||
'message' => '잘못된 요청',
|
'message' => $exception->getMessage() ?: '잘못된 요청',
|
||||||
'data' => null,
|
'data' => null,
|
||||||
], 400);
|
], 400);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
* - 두께 매핑 (normalizeThickness)
|
* - 두께 매핑 (normalizeThickness)
|
||||||
* - 면적 계산 (calculateArea)
|
* - 면적 계산 (calculateArea)
|
||||||
*
|
*
|
||||||
* @see docs/plans/5130-sam-data-migration-plan.md 섹션 4.5
|
* @see docs/dev_plans/5130-sam-data-migration-plan.md 섹션 4.5
|
||||||
*/
|
*/
|
||||||
class Legacy5130Calculator
|
class Legacy5130Calculator
|
||||||
{
|
{
|
||||||
|
|||||||
310
app/Helpers/SafeMathEvaluator.php
Normal file
310
app/Helpers/SafeMathEvaluator.php
Normal file
@@ -0,0 +1,310 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Helpers;
|
||||||
|
|
||||||
|
use InvalidArgumentException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* eval() 없이 산술 수식을 안전하게 계산하는 평가기
|
||||||
|
*
|
||||||
|
* Shunting-yard 알고리즘으로 중위 표기법 → 후위 표기법(RPN) 변환 후 계산
|
||||||
|
* 지원: 숫자, +, -, *, /, %, (, ), 단항 마이너스
|
||||||
|
*/
|
||||||
|
class SafeMathEvaluator
|
||||||
|
{
|
||||||
|
private const OPERATORS = ['+', '-', '*', '/', '%'];
|
||||||
|
|
||||||
|
private const PRECEDENCE = [
|
||||||
|
'+' => 2,
|
||||||
|
'-' => 2,
|
||||||
|
'*' => 3,
|
||||||
|
'/' => 3,
|
||||||
|
'%' => 3,
|
||||||
|
'UNARY_MINUS' => 4,
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 산술 수식을 계산하여 float 반환
|
||||||
|
*/
|
||||||
|
public static function calculate(string $expression): float
|
||||||
|
{
|
||||||
|
$expression = trim($expression);
|
||||||
|
|
||||||
|
if ($expression === '') {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$tokens = self::tokenize($expression);
|
||||||
|
|
||||||
|
if (empty($tokens)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$rpn = self::toRPN($tokens);
|
||||||
|
|
||||||
|
return self::evaluateRPN($rpn);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 비교식을 평가하여 bool 반환
|
||||||
|
* 예: "3000 <= 6000", "100 == 100", "5 > 3 && 2 < 4"
|
||||||
|
*/
|
||||||
|
public static function compare(string $expression): bool
|
||||||
|
{
|
||||||
|
$expression = trim($expression);
|
||||||
|
|
||||||
|
// && 논리 AND 처리
|
||||||
|
if (str_contains($expression, '&&')) {
|
||||||
|
$parts = explode('&&', $expression);
|
||||||
|
|
||||||
|
foreach ($parts as $part) {
|
||||||
|
if (! self::compare(trim($part))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// || 논리 OR 처리
|
||||||
|
if (str_contains($expression, '||')) {
|
||||||
|
$parts = explode('||', $expression);
|
||||||
|
|
||||||
|
foreach ($parts as $part) {
|
||||||
|
if (self::compare(trim($part))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 비교 연산자 추출 (2문자 먼저 검사)
|
||||||
|
$operators = ['>=', '<=', '!=', '==', '>', '<'];
|
||||||
|
|
||||||
|
foreach ($operators as $op) {
|
||||||
|
$pos = strpos($expression, $op);
|
||||||
|
if ($pos !== false) {
|
||||||
|
$left = self::calculate(substr($expression, 0, $pos));
|
||||||
|
$right = self::calculate(substr($expression, $pos + strlen($op)));
|
||||||
|
|
||||||
|
return match ($op) {
|
||||||
|
'>=' => $left >= $right,
|
||||||
|
'<=' => $left <= $right,
|
||||||
|
'!=' => $left != $right,
|
||||||
|
'==' => $left == $right,
|
||||||
|
'>' => $left > $right,
|
||||||
|
'<' => $left < $right,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 비교 연산자가 없으면 수치를 boolean으로 평가
|
||||||
|
return (bool) self::calculate($expression);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 수식 문자열을 토큰 배열로 분리
|
||||||
|
*/
|
||||||
|
private static function tokenize(string $expression): array
|
||||||
|
{
|
||||||
|
$tokens = [];
|
||||||
|
$len = strlen($expression);
|
||||||
|
$i = 0;
|
||||||
|
|
||||||
|
while ($i < $len) {
|
||||||
|
$char = $expression[$i];
|
||||||
|
|
||||||
|
// 공백 건너뛰기
|
||||||
|
if ($char === ' ' || $char === "\t") {
|
||||||
|
$i++;
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 숫자 (정수, 소수)
|
||||||
|
if (is_numeric($char) || ($char === '.' && $i + 1 < $len && is_numeric($expression[$i + 1]))) {
|
||||||
|
$num = '';
|
||||||
|
while ($i < $len && (is_numeric($expression[$i]) || $expression[$i] === '.')) {
|
||||||
|
$num .= $expression[$i];
|
||||||
|
$i++;
|
||||||
|
}
|
||||||
|
$tokens[] = ['type' => 'number', 'value' => (float) $num];
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 괄호
|
||||||
|
if ($char === '(') {
|
||||||
|
$tokens[] = ['type' => 'lparen'];
|
||||||
|
$i++;
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($char === ')') {
|
||||||
|
$tokens[] = ['type' => 'rparen'];
|
||||||
|
$i++;
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 연산자
|
||||||
|
if (in_array($char, self::OPERATORS)) {
|
||||||
|
// 단항 마이너스 판별: 맨 앞이거나, 앞이 연산자 또는 여는 괄호인 경우
|
||||||
|
if ($char === '-') {
|
||||||
|
$isUnary = empty($tokens)
|
||||||
|
|| $tokens[count($tokens) - 1]['type'] === 'operator'
|
||||||
|
|| $tokens[count($tokens) - 1]['type'] === 'lparen';
|
||||||
|
|
||||||
|
if ($isUnary) {
|
||||||
|
$tokens[] = ['type' => 'operator', 'value' => 'UNARY_MINUS'];
|
||||||
|
$i++;
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$tokens[] = ['type' => 'operator', 'value' => $char];
|
||||||
|
$i++;
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new InvalidArgumentException("허용되지 않는 문자: '{$char}' (위치 {$i})");
|
||||||
|
}
|
||||||
|
|
||||||
|
return $tokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 중위 표기법 토큰 → 후위 표기법(RPN) 변환 (Shunting-yard)
|
||||||
|
*/
|
||||||
|
private static function toRPN(array $tokens): array
|
||||||
|
{
|
||||||
|
$output = [];
|
||||||
|
$operatorStack = [];
|
||||||
|
|
||||||
|
foreach ($tokens as $token) {
|
||||||
|
if ($token['type'] === 'number') {
|
||||||
|
$output[] = $token;
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($token['type'] === 'operator') {
|
||||||
|
$op = $token['value'];
|
||||||
|
$prec = self::PRECEDENCE[$op] ?? 0;
|
||||||
|
|
||||||
|
while (! empty($operatorStack)) {
|
||||||
|
$top = end($operatorStack);
|
||||||
|
if ($top['type'] === 'lparen') {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
$topPrec = self::PRECEDENCE[$top['value']] ?? 0;
|
||||||
|
|
||||||
|
// 단항 연산자는 오른쪽 결합
|
||||||
|
if ($op === 'UNARY_MINUS') {
|
||||||
|
if ($topPrec > $prec) {
|
||||||
|
$output[] = array_pop($operatorStack);
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 이항 연산자는 왼쪽 결합 (같은 우선순위면 먼저 pop)
|
||||||
|
if ($topPrec >= $prec) {
|
||||||
|
$output[] = array_pop($operatorStack);
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$operatorStack[] = $token;
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($token['type'] === 'lparen') {
|
||||||
|
$operatorStack[] = $token;
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($token['type'] === 'rparen') {
|
||||||
|
while (! empty($operatorStack) && end($operatorStack)['type'] !== 'lparen') {
|
||||||
|
$output[] = array_pop($operatorStack);
|
||||||
|
}
|
||||||
|
if (empty($operatorStack)) {
|
||||||
|
throw new InvalidArgumentException('괄호 불일치: 여는 괄호 없음');
|
||||||
|
}
|
||||||
|
array_pop($operatorStack); // 여는 괄호 제거
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
while (! empty($operatorStack)) {
|
||||||
|
$top = array_pop($operatorStack);
|
||||||
|
if ($top['type'] === 'lparen') {
|
||||||
|
throw new InvalidArgumentException('괄호 불일치: 닫는 괄호 없음');
|
||||||
|
}
|
||||||
|
$output[] = $top;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $output;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 후위 표기법(RPN) 계산
|
||||||
|
*/
|
||||||
|
private static function evaluateRPN(array $rpn): float
|
||||||
|
{
|
||||||
|
$stack = [];
|
||||||
|
|
||||||
|
foreach ($rpn as $token) {
|
||||||
|
if ($token['type'] === 'number') {
|
||||||
|
$stack[] = $token['value'];
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($token['type'] === 'operator') {
|
||||||
|
$op = $token['value'];
|
||||||
|
|
||||||
|
// 단항 마이너스
|
||||||
|
if ($op === 'UNARY_MINUS') {
|
||||||
|
if (empty($stack)) {
|
||||||
|
throw new InvalidArgumentException('수식 오류: 단항 마이너스 피연산자 없음');
|
||||||
|
}
|
||||||
|
$stack[] = -array_pop($stack);
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 이항 연산자
|
||||||
|
if (count($stack) < 2) {
|
||||||
|
throw new InvalidArgumentException('수식 오류: 피연산자 부족');
|
||||||
|
}
|
||||||
|
|
||||||
|
$right = array_pop($stack);
|
||||||
|
$left = array_pop($stack);
|
||||||
|
|
||||||
|
$stack[] = match ($op) {
|
||||||
|
'+' => $left + $right,
|
||||||
|
'-' => $left - $right,
|
||||||
|
'*' => $left * $right,
|
||||||
|
'/' => $right != 0 ? $left / $right : throw new InvalidArgumentException('0으로 나눌 수 없음'),
|
||||||
|
'%' => $right != 0 ? fmod($left, $right) : throw new InvalidArgumentException('0으로 나눌 수 없음'),
|
||||||
|
default => throw new InvalidArgumentException("알 수 없는 연산자: {$op}"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count($stack) !== 1) {
|
||||||
|
throw new InvalidArgumentException('수식 오류: 결과가 하나가 아님');
|
||||||
|
}
|
||||||
|
|
||||||
|
return (float) $stack[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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'));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,13 +4,18 @@
|
|||||||
|
|
||||||
use App\Helpers\ApiResponse;
|
use App\Helpers\ApiResponse;
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\Barobill\BarobillBankTransaction;
|
||||||
|
use App\Models\Barobill\BarobillCardTransaction;
|
||||||
|
use App\Models\Barobill\BarobillMember;
|
||||||
|
use App\Services\Barobill\BarobillSoapService;
|
||||||
use App\Services\BarobillService;
|
use App\Services\BarobillService;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
class BarobillController extends Controller
|
class BarobillController extends Controller
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private BarobillService $barobillService
|
private BarobillService $barobillService,
|
||||||
|
private BarobillSoapService $soapService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -19,17 +24,43 @@ public function __construct(
|
|||||||
public function status()
|
public function status()
|
||||||
{
|
{
|
||||||
return ApiResponse::handle(function () {
|
return ApiResponse::handle(function () {
|
||||||
|
$tenantId = app('tenant_id');
|
||||||
$setting = $this->barobillService->getSetting();
|
$setting = $this->barobillService->getSetting();
|
||||||
|
$member = BarobillMember::withoutGlobalScopes()
|
||||||
|
->where('tenant_id', $tenantId)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
$accountCount = 0;
|
||||||
|
$cardCount = 0;
|
||||||
|
|
||||||
|
if ($member) {
|
||||||
|
$accountCount = BarobillBankTransaction::withoutGlobalScopes()
|
||||||
|
->where('tenant_id', $tenantId)
|
||||||
|
->distinct('bank_account_num')
|
||||||
|
->count('bank_account_num');
|
||||||
|
|
||||||
|
$cardCount = BarobillCardTransaction::withoutGlobalScopes()
|
||||||
|
->where('tenant_id', $tenantId)
|
||||||
|
->distinct('card_num')
|
||||||
|
->count('card_num');
|
||||||
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'bank_service_count' => 0,
|
'bank_service_count' => $accountCount,
|
||||||
'account_link_count' => 0,
|
'account_link_count' => $accountCount,
|
||||||
'member' => $setting ? [
|
'card_count' => $cardCount,
|
||||||
|
'member' => $member ? [
|
||||||
|
'barobill_id' => $member->barobill_id,
|
||||||
|
'biz_no' => $member->formatted_biz_no,
|
||||||
|
'corp_name' => $member->corp_name,
|
||||||
|
'status' => $member->status,
|
||||||
|
'server_mode' => $member->server_mode ?? 'test',
|
||||||
|
] : ($setting ? [
|
||||||
'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 +117,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 +141,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 +151,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 +161,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')
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
306
app/Http/Controllers/Api/V1/BarobillSyncController.php
Normal file
306
app/Http/Controllers/Api/V1/BarobillSyncController.php
Normal file
@@ -0,0 +1,306 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api\V1;
|
||||||
|
|
||||||
|
use App\Helpers\ApiResponse;
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\Barobill\BarobillMember;
|
||||||
|
use App\Services\Barobill\BarobillBankSyncService;
|
||||||
|
use App\Services\Barobill\BarobillCardSyncService;
|
||||||
|
use App\Services\Barobill\BarobillSoapService;
|
||||||
|
use App\Services\Barobill\HometaxSyncService;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class BarobillSyncController extends Controller
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private BarobillSoapService $soapService,
|
||||||
|
private BarobillBankSyncService $bankSyncService,
|
||||||
|
private BarobillCardSyncService $cardSyncService,
|
||||||
|
private HometaxSyncService $hometaxSyncService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 수동 은행 동기화
|
||||||
|
*/
|
||||||
|
public function syncBank(Request $request)
|
||||||
|
{
|
||||||
|
$data = $request->validate([
|
||||||
|
'start_date' => 'nullable|date_format:Ymd',
|
||||||
|
'end_date' => 'nullable|date_format:Ymd',
|
||||||
|
]);
|
||||||
|
|
||||||
|
return ApiResponse::handle(function () use ($data) {
|
||||||
|
$tenantId = app('tenant_id');
|
||||||
|
$startDate = $data['start_date'] ?? Carbon::now()->subMonth()->format('Ymd');
|
||||||
|
$endDate = $data['end_date'] ?? Carbon::now()->format('Ymd');
|
||||||
|
|
||||||
|
return $this->bankSyncService->syncIfNeeded($tenantId, $startDate, $endDate);
|
||||||
|
}, __('message.fetched'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 수동 카드 동기화
|
||||||
|
*/
|
||||||
|
public function syncCard(Request $request)
|
||||||
|
{
|
||||||
|
$data = $request->validate([
|
||||||
|
'start_date' => 'nullable|date_format:Ymd',
|
||||||
|
'end_date' => 'nullable|date_format:Ymd',
|
||||||
|
]);
|
||||||
|
|
||||||
|
return ApiResponse::handle(function () use ($data) {
|
||||||
|
$tenantId = app('tenant_id');
|
||||||
|
$startDate = $data['start_date'] ?? Carbon::now()->subMonth()->format('Ymd');
|
||||||
|
$endDate = $data['end_date'] ?? Carbon::now()->format('Ymd');
|
||||||
|
|
||||||
|
return $this->cardSyncService->syncCardTransactions($tenantId, $startDate, $endDate);
|
||||||
|
}, __('message.fetched'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 수동 홈택스 동기화
|
||||||
|
*/
|
||||||
|
public function syncHometax(Request $request)
|
||||||
|
{
|
||||||
|
$data = $request->validate([
|
||||||
|
'invoices' => 'required|array',
|
||||||
|
'invoices.*.ntsConfirmNum' => 'required|string',
|
||||||
|
'invoice_type' => 'required|in:sales,purchase',
|
||||||
|
]);
|
||||||
|
|
||||||
|
return ApiResponse::handle(function () use ($data) {
|
||||||
|
$tenantId = app('tenant_id');
|
||||||
|
|
||||||
|
return $this->hometaxSyncService->syncInvoices(
|
||||||
|
$data['invoices'],
|
||||||
|
$tenantId,
|
||||||
|
$data['invoice_type']
|
||||||
|
);
|
||||||
|
}, __('message.fetched'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 바로빌 등록계좌 목록 (SOAP 실시간)
|
||||||
|
*/
|
||||||
|
public function accounts()
|
||||||
|
{
|
||||||
|
return ApiResponse::handle(function () {
|
||||||
|
$member = $this->getMember();
|
||||||
|
if (! $member) {
|
||||||
|
return ['accounts' => [], 'message' => '바로빌 회원 정보 없음'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->soapService->initForMember($member);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'accounts' => $this->bankSyncService->getRegisteredAccounts($member),
|
||||||
|
];
|
||||||
|
}, __('message.fetched'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 바로빌 등록카드 목록 (SOAP 실시간)
|
||||||
|
*/
|
||||||
|
public function cards()
|
||||||
|
{
|
||||||
|
return ApiResponse::handle(function () {
|
||||||
|
$member = $this->getMember();
|
||||||
|
if (! $member) {
|
||||||
|
return ['cards' => [], 'message' => '바로빌 회원 정보 없음'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->soapService->initForMember($member);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'cards' => $this->cardSyncService->getRegisteredCards($member),
|
||||||
|
];
|
||||||
|
}, __('message.fetched'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 인증서 상태 조회 (만료일, 유효성)
|
||||||
|
*/
|
||||||
|
public function certificate()
|
||||||
|
{
|
||||||
|
return ApiResponse::handle(function () {
|
||||||
|
$member = $this->getMember();
|
||||||
|
if (! $member) {
|
||||||
|
return ['certificate' => null, 'message' => '바로빌 회원 정보 없음'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->soapService->initForMember($member);
|
||||||
|
$corpNum = $member->biz_no;
|
||||||
|
|
||||||
|
$valid = $this->soapService->checkCertificateValid($corpNum);
|
||||||
|
$expireDate = $this->soapService->getCertificateExpireDate($corpNum);
|
||||||
|
$registDate = $this->soapService->getCertificateRegistDate($corpNum);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'certificate' => [
|
||||||
|
'is_valid' => $valid['success'] && ($valid['data'] ?? 0) >= 0,
|
||||||
|
'expire_date' => $expireDate['success'] ? ($expireDate['data'] ?? null) : null,
|
||||||
|
'regist_date' => $registDate['success'] ? ($registDate['data'] ?? null) : null,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}, __('message.fetched'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 충전잔액 조회
|
||||||
|
*/
|
||||||
|
public function balance()
|
||||||
|
{
|
||||||
|
return ApiResponse::handle(function () {
|
||||||
|
$member = $this->getMember();
|
||||||
|
if (! $member) {
|
||||||
|
return ['balance' => null, 'message' => '바로빌 회원 정보 없음'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->soapService->initForMember($member);
|
||||||
|
$result = $this->soapService->getBalanceCostAmount($member->biz_no);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'balance' => $result['success'] ? ($result['data'] ?? 0) : null,
|
||||||
|
'success' => $result['success'],
|
||||||
|
'error' => $result['error'] ?? null,
|
||||||
|
];
|
||||||
|
}, __('message.fetched'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 바로빌 회원 등록 (SOAP RegistCorp)
|
||||||
|
*/
|
||||||
|
public function registerMember(Request $request)
|
||||||
|
{
|
||||||
|
$data = $request->validate([
|
||||||
|
'biz_no' => 'required|string|size:10',
|
||||||
|
'corp_name' => 'required|string',
|
||||||
|
'ceo_name' => 'required|string',
|
||||||
|
'biz_type' => 'nullable|string',
|
||||||
|
'biz_class' => 'nullable|string',
|
||||||
|
'addr' => 'nullable|string',
|
||||||
|
'barobill_id' => 'required|string',
|
||||||
|
'barobill_pwd' => 'required|string',
|
||||||
|
'manager_name' => 'nullable|string',
|
||||||
|
'manager_hp' => 'nullable|string',
|
||||||
|
'manager_email' => 'nullable|email',
|
||||||
|
]);
|
||||||
|
|
||||||
|
return ApiResponse::handle(function () use ($data) {
|
||||||
|
$tenantId = app('tenant_id');
|
||||||
|
$this->soapService->initForMember(
|
||||||
|
BarobillMember::withoutGlobalScopes()->where('tenant_id', $tenantId)->first()
|
||||||
|
?? new BarobillMember(['server_mode' => 'test'])
|
||||||
|
);
|
||||||
|
|
||||||
|
$result = $this->soapService->registCorp($data);
|
||||||
|
|
||||||
|
if ($result['success']) {
|
||||||
|
BarobillMember::withoutGlobalScopes()->updateOrCreate(
|
||||||
|
['tenant_id' => $tenantId],
|
||||||
|
[
|
||||||
|
'biz_no' => $data['biz_no'],
|
||||||
|
'corp_name' => $data['corp_name'],
|
||||||
|
'ceo_name' => $data['ceo_name'],
|
||||||
|
'biz_type' => $data['biz_type'] ?? null,
|
||||||
|
'biz_class' => $data['biz_class'] ?? null,
|
||||||
|
'addr' => $data['addr'] ?? null,
|
||||||
|
'barobill_id' => $data['barobill_id'],
|
||||||
|
'barobill_pwd' => $data['barobill_pwd'],
|
||||||
|
'manager_name' => $data['manager_name'] ?? null,
|
||||||
|
'manager_hp' => $data['manager_hp'] ?? null,
|
||||||
|
'manager_email' => $data['manager_email'] ?? null,
|
||||||
|
'status' => 'active',
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}, __('message.created'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 바로빌 회원 수정 (SOAP UpdateCorpInfo)
|
||||||
|
*/
|
||||||
|
public function updateMember(Request $request)
|
||||||
|
{
|
||||||
|
$data = $request->validate([
|
||||||
|
'corp_name' => 'required|string',
|
||||||
|
'ceo_name' => 'required|string',
|
||||||
|
'biz_type' => 'nullable|string',
|
||||||
|
'biz_class' => 'nullable|string',
|
||||||
|
'addr' => 'nullable|string',
|
||||||
|
'manager_name' => 'nullable|string',
|
||||||
|
'manager_hp' => 'nullable|string',
|
||||||
|
'manager_email' => 'nullable|email',
|
||||||
|
]);
|
||||||
|
|
||||||
|
return ApiResponse::handle(function () use ($data) {
|
||||||
|
$member = $this->getMember();
|
||||||
|
if (! $member) {
|
||||||
|
return ['error' => 'NO_MEMBER', 'code' => 404, 'message' => '바로빌 회원 정보 없음'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->soapService->initForMember($member);
|
||||||
|
|
||||||
|
$data['biz_no'] = $member->biz_no;
|
||||||
|
$result = $this->soapService->updateCorpInfo($data);
|
||||||
|
|
||||||
|
if ($result['success']) {
|
||||||
|
$member->update([
|
||||||
|
'corp_name' => $data['corp_name'],
|
||||||
|
'ceo_name' => $data['ceo_name'],
|
||||||
|
'biz_type' => $data['biz_type'] ?? $member->biz_type,
|
||||||
|
'biz_class' => $data['biz_class'] ?? $member->biz_class,
|
||||||
|
'addr' => $data['addr'] ?? $member->addr,
|
||||||
|
'manager_name' => $data['manager_name'] ?? $member->manager_name,
|
||||||
|
'manager_hp' => $data['manager_hp'] ?? $member->manager_hp,
|
||||||
|
'manager_email' => $data['manager_email'] ?? $member->manager_email,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}, __('message.updated'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 바로빌 회원 상태 (SOAP GetCorpState)
|
||||||
|
*/
|
||||||
|
public function memberStatus()
|
||||||
|
{
|
||||||
|
return ApiResponse::handle(function () {
|
||||||
|
$member = $this->getMember();
|
||||||
|
if (! $member) {
|
||||||
|
return ['status' => null, 'message' => '바로빌 회원 정보 없음'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->soapService->initForMember($member);
|
||||||
|
$result = $this->soapService->getCorpState($member->biz_no);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'member' => [
|
||||||
|
'biz_no' => $member->formatted_biz_no,
|
||||||
|
'corp_name' => $member->corp_name,
|
||||||
|
'status' => $member->status,
|
||||||
|
'server_mode' => $member->server_mode,
|
||||||
|
],
|
||||||
|
'barobill_state' => $result['success'] ? $result['data'] : null,
|
||||||
|
'error' => $result['error'] ?? null,
|
||||||
|
];
|
||||||
|
}, __('message.fetched'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 현재 테넌트의 바로빌 회원 조회
|
||||||
|
*/
|
||||||
|
private function getMember(): ?BarobillMember
|
||||||
|
{
|
||||||
|
$tenantId = app('tenant_id');
|
||||||
|
|
||||||
|
return BarobillMember::withoutGlobalScopes()
|
||||||
|
->where('tenant_id', $tenantId)
|
||||||
|
->first();
|
||||||
|
}
|
||||||
|
}
|
||||||
119
app/Http/Controllers/Api/V1/BendingController.php
Normal file
119
app/Http/Controllers/Api/V1/BendingController.php
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api\V1;
|
||||||
|
|
||||||
|
use App\Helpers\ApiResponse;
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\Tenants\Receiving;
|
||||||
|
use App\Services\BendingCodeService;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class BendingController extends Controller
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly BendingCodeService $service
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 절곡품 코드맵 조회 (캐스케이딩 드롭다운용)
|
||||||
|
*/
|
||||||
|
public function codeMap(): JsonResponse
|
||||||
|
{
|
||||||
|
return ApiResponse::handle(function () {
|
||||||
|
return $this->service->getCodeMap();
|
||||||
|
}, __('message.fetched'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 드롭다운 선택 → 품목 매핑 조회
|
||||||
|
*/
|
||||||
|
public function resolveItem(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
return ApiResponse::handle(function () use ($request) {
|
||||||
|
$prodCode = $request->query('prod');
|
||||||
|
$specCode = $request->query('spec');
|
||||||
|
$lengthCode = $request->query('length');
|
||||||
|
|
||||||
|
if (! $prodCode || ! $specCode || ! $lengthCode) {
|
||||||
|
return ['error' => 'MISSING_PARAMS', 'code' => 400, 'message' => 'prod, spec, length 파라미터가 필요합니다.'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$item = $this->service->resolveItem($prodCode, $specCode, $lengthCode);
|
||||||
|
|
||||||
|
if (! $item) {
|
||||||
|
return ['error' => 'NOT_MAPPED', 'code' => 404, 'message' => '해당 조합에 매핑된 품목이 없습니다.'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $item;
|
||||||
|
}, __('message.fetched'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 원자재 LOT 목록 조회 (수입검사 완료 입고 기준)
|
||||||
|
*
|
||||||
|
* 재질(material)이 일치하는 입고 LOT 목록 반환
|
||||||
|
*/
|
||||||
|
public function materialLots(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
return ApiResponse::handle(function () use ($request) {
|
||||||
|
$material = $request->query('material');
|
||||||
|
|
||||||
|
$query = Receiving::where('status', 'completed')
|
||||||
|
->whereNotNull('lot_no')
|
||||||
|
->where('lot_no', '!=', '');
|
||||||
|
|
||||||
|
// 재질(item_name 또는 specification)으로 필터링
|
||||||
|
if ($material) {
|
||||||
|
$query->where(function ($q) use ($material) {
|
||||||
|
$q->where('item_name', 'LIKE', "%{$material}%")
|
||||||
|
->orWhere('specification', 'LIKE', "%{$material}%");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return $query->select([
|
||||||
|
'id',
|
||||||
|
'lot_no',
|
||||||
|
'supplier_lot',
|
||||||
|
'item_name',
|
||||||
|
'specification',
|
||||||
|
'receiving_qty',
|
||||||
|
'receiving_date',
|
||||||
|
'supplier',
|
||||||
|
'options',
|
||||||
|
])
|
||||||
|
->orderByDesc('receiving_date')
|
||||||
|
->limit(50)
|
||||||
|
->get();
|
||||||
|
}, __('message.fetched'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* LOT 번호 생성 (프리뷰 + 일련번호 확정)
|
||||||
|
*/
|
||||||
|
public function generateLotNumber(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
return ApiResponse::handle(function () use ($request) {
|
||||||
|
$prodCode = $request->input('prod_code');
|
||||||
|
$specCode = $request->input('spec_code');
|
||||||
|
$lengthCode = $request->input('length_code');
|
||||||
|
$regDate = $request->input('reg_date', now()->toDateString());
|
||||||
|
|
||||||
|
if (! $prodCode || ! $specCode || ! $lengthCode) {
|
||||||
|
return ['error' => 'MISSING_PARAMS', 'code' => 400, 'message' => 'prod_code, spec_code, length_code가 필요합니다.'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$dateCode = BendingCodeService::generateDateCode($regDate);
|
||||||
|
$lotBase = "{$prodCode}{$specCode}{$dateCode}-{$lengthCode}";
|
||||||
|
$lotNumber = $this->service->generateLotNumber($lotBase);
|
||||||
|
$material = BendingCodeService::getMaterial($prodCode, $specCode);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'lot_base' => $lotBase,
|
||||||
|
'lot_number' => $lotNumber,
|
||||||
|
'date_code' => $dateCode,
|
||||||
|
'material' => $material,
|
||||||
|
];
|
||||||
|
}, __('message.fetched'));
|
||||||
|
}
|
||||||
|
}
|
||||||
95
app/Http/Controllers/Api/V1/BendingItemController.php
Normal file
95
app/Http/Controllers/Api/V1/BendingItemController.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\Api\V1\BendingItemIndexRequest;
|
||||||
|
use App\Http\Requests\Api\V1\BendingItemStoreRequest;
|
||||||
|
use App\Http\Requests\Api\V1\BendingItemUpdateRequest;
|
||||||
|
use App\Http\Resources\Api\V1\BendingItemResource;
|
||||||
|
use App\Services\BendingItemService;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class BendingItemController extends Controller
|
||||||
|
{
|
||||||
|
public function __construct(private BendingItemService $service) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bearer 토큰 없이 X-TENANT-ID 헤더로 컨텍스트 설정
|
||||||
|
*/
|
||||||
|
private function ensureContext(Request $request): void
|
||||||
|
{
|
||||||
|
if (! app()->bound('tenant_id') || ! app('tenant_id')) {
|
||||||
|
$tenantId = (int) ($request->header('X-TENANT-ID') ?: 287);
|
||||||
|
app()->instance('tenant_id', $tenantId);
|
||||||
|
}
|
||||||
|
if (! app()->bound('api_user') || ! app('api_user')) {
|
||||||
|
// mng에서 Bearer 토큰 없이 호출 시 시스템 사용자(1)로 설정
|
||||||
|
app()->instance('api_user', 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function index(BendingItemIndexRequest $request): JsonResponse
|
||||||
|
{
|
||||||
|
$this->ensureContext($request);
|
||||||
|
|
||||||
|
return ApiResponse::handle(function () use ($request) {
|
||||||
|
$paginator = $this->service->list($request->validated());
|
||||||
|
$paginator->getCollection()->transform(fn ($item) => (new BendingItemResource($item))->resolve());
|
||||||
|
|
||||||
|
return $paginator;
|
||||||
|
}, __('message.fetched'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function filters(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$this->ensureContext($request);
|
||||||
|
|
||||||
|
return ApiResponse::handle(
|
||||||
|
fn () => $this->service->filters(),
|
||||||
|
__('message.fetched')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function show(Request $request, int $id): JsonResponse
|
||||||
|
{
|
||||||
|
$this->ensureContext($request);
|
||||||
|
|
||||||
|
return ApiResponse::handle(
|
||||||
|
fn () => new BendingItemResource($this->service->find($id)),
|
||||||
|
__('message.fetched')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(BendingItemStoreRequest $request): JsonResponse
|
||||||
|
{
|
||||||
|
$this->ensureContext($request);
|
||||||
|
|
||||||
|
return ApiResponse::handle(
|
||||||
|
fn () => new BendingItemResource($this->service->create($request->validated())),
|
||||||
|
__('message.created')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(BendingItemUpdateRequest $request, int $id): JsonResponse
|
||||||
|
{
|
||||||
|
$this->ensureContext($request);
|
||||||
|
|
||||||
|
return ApiResponse::handle(
|
||||||
|
fn () => new BendingItemResource($this->service->update($id, $request->validated())),
|
||||||
|
__('message.updated')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy(Request $request, int $id): JsonResponse
|
||||||
|
{
|
||||||
|
$this->ensureContext($request);
|
||||||
|
|
||||||
|
return ApiResponse::handle(
|
||||||
|
fn () => $this->service->delete($id),
|
||||||
|
__('message.deleted')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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'));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,6 +20,16 @@ public function index(Request $request)
|
|||||||
}, __('message.client.fetched'));
|
}, __('message.client.fetched'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 거래처 간단 목록 (id, name만 반환) - vendors 엔드포인트용
|
||||||
|
*/
|
||||||
|
public function vendors(Request $request)
|
||||||
|
{
|
||||||
|
return ApiResponse::handle(function () use ($request) {
|
||||||
|
return $this->service->vendors($request->all());
|
||||||
|
}, __('message.client.fetched'));
|
||||||
|
}
|
||||||
|
|
||||||
public function show(int $id)
|
public function show(int $id)
|
||||||
{
|
{
|
||||||
return ApiResponse::handle(function () use ($id) {
|
return ApiResponse::handle(function () use ($id) {
|
||||||
|
|||||||
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'));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -74,6 +74,22 @@ public function destroy(int $id): JsonResponse
|
|||||||
}, __('message.deleted'));
|
}, __('message.deleted'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* rendered_html 스냅샷 저장 (Lazy Snapshot)
|
||||||
|
* PATCH /v1/documents/{id}/snapshot
|
||||||
|
*/
|
||||||
|
public function patchSnapshot(int $id, UpdateRequest $request): JsonResponse
|
||||||
|
{
|
||||||
|
return ApiResponse::handle(function () use ($id, $request) {
|
||||||
|
$renderedHtml = $request->validated()['rendered_html'] ?? null;
|
||||||
|
if (! $renderedHtml) {
|
||||||
|
throw new \Symfony\Component\HttpKernel\Exception\BadRequestHttpException('rendered_html is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->service->patchSnapshot($id, $renderedHtml);
|
||||||
|
}, __('message.updated'));
|
||||||
|
}
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// FQC 일괄생성 (제품검사)
|
// FQC 일괄생성 (제품검사)
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|||||||
@@ -12,6 +12,20 @@
|
|||||||
|
|
||||||
class FileStorageController extends Controller
|
class FileStorageController extends Controller
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* Bearer 토큰 없이 X-TENANT-ID 헤더로 컨텍스트 설정 (MNG 프록시 호출용)
|
||||||
|
*/
|
||||||
|
private function ensureContext(Request $request): void
|
||||||
|
{
|
||||||
|
if (! app()->bound('tenant_id') || ! app('tenant_id')) {
|
||||||
|
$tenantId = (int) ($request->header('X-TENANT-ID') ?: 287);
|
||||||
|
app()->instance('tenant_id', $tenantId);
|
||||||
|
}
|
||||||
|
if (! app()->bound('api_user') || ! app('api_user')) {
|
||||||
|
app()->instance('api_user', 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Upload file to temp
|
* Upload file to temp
|
||||||
*/
|
*/
|
||||||
@@ -83,14 +97,29 @@ public function trash()
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Download file
|
* Download file (attachment)
|
||||||
*/
|
*/
|
||||||
public function download(int $id)
|
public function download(int $id, Request $request)
|
||||||
{
|
{
|
||||||
|
$this->ensureContext($request);
|
||||||
|
|
||||||
$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, Request $request)
|
||||||
|
{
|
||||||
|
$this->ensureContext($request);
|
||||||
|
|
||||||
|
$service = new FileStorageService;
|
||||||
|
$file = $service->getFile($id);
|
||||||
|
|
||||||
|
return $file->download(inline: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
89
app/Http/Controllers/Api/V1/GuiderailModelController.php
Normal file
89
app/Http/Controllers/Api/V1/GuiderailModelController.php
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api\V1;
|
||||||
|
|
||||||
|
use App\Helpers\ApiResponse;
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Http\Resources\Api\V1\GuiderailModelResource;
|
||||||
|
use App\Services\GuiderailModelService;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class GuiderailModelController extends Controller
|
||||||
|
{
|
||||||
|
public function __construct(private GuiderailModelService $service) {}
|
||||||
|
|
||||||
|
private function ensureContext(Request $request): void
|
||||||
|
{
|
||||||
|
if (! app()->bound('tenant_id') || ! app('tenant_id')) {
|
||||||
|
$tenantId = (int) ($request->header('X-TENANT-ID') ?: 287);
|
||||||
|
app()->instance('tenant_id', $tenantId);
|
||||||
|
}
|
||||||
|
if (! app()->bound('api_user') || ! app('api_user')) {
|
||||||
|
app()->instance('api_user', 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function index(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$this->ensureContext($request);
|
||||||
|
|
||||||
|
return ApiResponse::handle(function () use ($request) {
|
||||||
|
$params = $request->only(['item_category', 'item_sep', 'model_UA', 'check_type', 'model_name', 'search', 'page', 'size']);
|
||||||
|
$paginator = $this->service->list($params);
|
||||||
|
$paginator->getCollection()->transform(fn ($item) => (new GuiderailModelResource($item))->resolve());
|
||||||
|
|
||||||
|
return $paginator;
|
||||||
|
}, __('message.fetched'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function filters(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$this->ensureContext($request);
|
||||||
|
|
||||||
|
return ApiResponse::handle(
|
||||||
|
fn () => $this->service->filters(),
|
||||||
|
__('message.fetched')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function show(Request $request, int $id): JsonResponse
|
||||||
|
{
|
||||||
|
$this->ensureContext($request);
|
||||||
|
|
||||||
|
return ApiResponse::handle(
|
||||||
|
fn () => new GuiderailModelResource($this->service->find($id)),
|
||||||
|
__('message.fetched')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$this->ensureContext($request);
|
||||||
|
|
||||||
|
return ApiResponse::handle(
|
||||||
|
fn () => new GuiderailModelResource($this->service->create($request->all())),
|
||||||
|
__('message.created')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(Request $request, int $id): JsonResponse
|
||||||
|
{
|
||||||
|
$this->ensureContext($request);
|
||||||
|
|
||||||
|
return ApiResponse::handle(
|
||||||
|
fn () => new GuiderailModelResource($this->service->update($id, $request->all())),
|
||||||
|
__('message.updated')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy(Request $request, int $id): JsonResponse
|
||||||
|
{
|
||||||
|
$this->ensureContext($request);
|
||||||
|
|
||||||
|
return ApiResponse::handle(
|
||||||
|
fn () => $this->service->delete($id),
|
||||||
|
__('message.deleted')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
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'));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,6 +28,7 @@ public function index(Request $request)
|
|||||||
'category_id' => $request->input('category_id'),
|
'category_id' => $request->input('category_id'),
|
||||||
'item_type' => $request->input('type') ?? $request->input('item_type'),
|
'item_type' => $request->input('type') ?? $request->input('item_type'),
|
||||||
'item_category' => $request->input('item_category'),
|
'item_category' => $request->input('item_category'),
|
||||||
|
'bom_category' => $request->input('bom_category'),
|
||||||
'group_id' => $request->input('group_id'),
|
'group_id' => $request->input('group_id'),
|
||||||
'active' => $request->input('is_active') ?? $request->input('active'),
|
'active' => $request->input('is_active') ?? $request->input('active'),
|
||||||
'has_bom' => $request->input('has_bom'),
|
'has_bom' => $request->input('has_bom'),
|
||||||
|
|||||||
@@ -26,6 +26,20 @@ class ItemsFileController extends Controller
|
|||||||
*/
|
*/
|
||||||
private const ITEM_GROUP_ID = '1';
|
private const ITEM_GROUP_ID = '1';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bearer 토큰 없이 X-TENANT-ID 헤더로 컨텍스트 설정 (MNG 프록시 호출용)
|
||||||
|
*/
|
||||||
|
private function ensureContext(Request $request): void
|
||||||
|
{
|
||||||
|
if (! app()->bound('tenant_id') || ! app('tenant_id')) {
|
||||||
|
$tenantId = (int) ($request->header('X-TENANT-ID') ?: 287);
|
||||||
|
app()->instance('tenant_id', $tenantId);
|
||||||
|
}
|
||||||
|
if (! app()->bound('api_user') || ! app('api_user')) {
|
||||||
|
app()->instance('api_user', 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 파일 목록 조회
|
* 파일 목록 조회
|
||||||
*
|
*
|
||||||
@@ -33,6 +47,7 @@ class ItemsFileController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function index(int $id, Request $request)
|
public function index(int $id, Request $request)
|
||||||
{
|
{
|
||||||
|
$this->ensureContext($request);
|
||||||
return ApiResponse::handle(function () use ($id, $request) {
|
return ApiResponse::handle(function () use ($id, $request) {
|
||||||
$tenantId = app('tenant_id');
|
$tenantId = app('tenant_id');
|
||||||
$fieldKey = $request->input('field_key');
|
$fieldKey = $request->input('field_key');
|
||||||
@@ -69,6 +84,7 @@ public function index(int $id, Request $request)
|
|||||||
*/
|
*/
|
||||||
public function upload(int $id, ItemFileUploadRequest $request)
|
public function upload(int $id, ItemFileUploadRequest $request)
|
||||||
{
|
{
|
||||||
|
$this->ensureContext($request);
|
||||||
return ApiResponse::handle(function () use ($id, $request) {
|
return ApiResponse::handle(function () use ($id, $request) {
|
||||||
$tenantId = app('tenant_id');
|
$tenantId = app('tenant_id');
|
||||||
$userId = auth()->id() ?? app('api_user');
|
$userId = auth()->id() ?? app('api_user');
|
||||||
@@ -109,7 +125,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();
|
||||||
@@ -152,6 +168,7 @@ public function upload(int $id, ItemFileUploadRequest $request)
|
|||||||
*/
|
*/
|
||||||
public function delete(int $id, mixed $fileId, Request $request)
|
public function delete(int $id, mixed $fileId, Request $request)
|
||||||
{
|
{
|
||||||
|
$this->ensureContext($request);
|
||||||
$fileId = (int) $fileId;
|
$fileId = (int) $fileId;
|
||||||
|
|
||||||
return ApiResponse::handle(function () use ($id, $fileId) {
|
return ApiResponse::handle(function () use ($id, $fileId) {
|
||||||
|
|||||||
@@ -30,10 +30,10 @@ public function index(Request $request)
|
|||||||
/**
|
/**
|
||||||
* 통계 조회
|
* 통계 조회
|
||||||
*/
|
*/
|
||||||
public function stats()
|
public function stats(Request $request)
|
||||||
{
|
{
|
||||||
return ApiResponse::handle(function () {
|
return ApiResponse::handle(function () use ($request) {
|
||||||
return $this->service->stats();
|
return $this->service->stats($request->input('order_type'));
|
||||||
}, __('message.order.fetched'));
|
}, __('message.order.fetched'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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'));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 급여 설정 조회
|
* 급여 설정 조회
|
||||||
*/
|
*/
|
||||||
|
|||||||
67
app/Http/Controllers/Api/V1/PerformanceReportController.php
Normal file
67
app/Http/Controllers/Api/V1/PerformanceReportController.php
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api\V1;
|
||||||
|
|
||||||
|
use App\Helpers\ApiResponse;
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Http\Requests\Quality\PerformanceReportConfirmRequest;
|
||||||
|
use App\Http\Requests\Quality\PerformanceReportMemoRequest;
|
||||||
|
use App\Services\PerformanceReportService;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class PerformanceReportController extends Controller
|
||||||
|
{
|
||||||
|
public function __construct(private PerformanceReportService $service) {}
|
||||||
|
|
||||||
|
public function index(Request $request)
|
||||||
|
{
|
||||||
|
return ApiResponse::handle(function () use ($request) {
|
||||||
|
return $this->service->index($request->all());
|
||||||
|
}, __('message.fetched'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stats(Request $request)
|
||||||
|
{
|
||||||
|
return ApiResponse::handle(function () use ($request) {
|
||||||
|
return $this->service->stats($request->all());
|
||||||
|
}, __('message.fetched'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function confirm(PerformanceReportConfirmRequest $request)
|
||||||
|
{
|
||||||
|
return ApiResponse::handle(function () use ($request) {
|
||||||
|
return $this->service->confirm($request->validated()['ids']);
|
||||||
|
}, __('message.updated'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function unconfirm(PerformanceReportConfirmRequest $request)
|
||||||
|
{
|
||||||
|
return ApiResponse::handle(function () use ($request) {
|
||||||
|
return $this->service->unconfirm($request->validated()['ids']);
|
||||||
|
}, __('message.updated'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updateMemo(PerformanceReportMemoRequest $request)
|
||||||
|
{
|
||||||
|
return ApiResponse::handle(function () use ($request) {
|
||||||
|
$data = $request->validated();
|
||||||
|
|
||||||
|
return $this->service->updateMemo($data['ids'], $data['memo']);
|
||||||
|
}, __('message.updated'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function missing(Request $request)
|
||||||
|
{
|
||||||
|
return ApiResponse::handle(function () use ($request) {
|
||||||
|
return $this->service->missing($request->all());
|
||||||
|
}, __('message.fetched'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function exportExcel(Request $request)
|
||||||
|
{
|
||||||
|
$year = (int) $request->input('year', now()->year);
|
||||||
|
$quarter = (int) $request->input('quarter', ceil(now()->month / 3));
|
||||||
|
|
||||||
|
return $this->service->exportConfirmed($year, $quarter);
|
||||||
|
}
|
||||||
|
}
|
||||||
50
app/Http/Controllers/Api/V1/ProductionOrderController.php
Normal file
50
app/Http/Controllers/Api/V1/ProductionOrderController.php
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api\V1;
|
||||||
|
|
||||||
|
use App\Helpers\ApiResponse;
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Http\Requests\ProductionOrder\ProductionOrderIndexRequest;
|
||||||
|
use App\Services\ProductionOrderService;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
|
||||||
|
class ProductionOrderController extends Controller
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly ProductionOrderService $service
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 생산지시 목록 조회
|
||||||
|
*/
|
||||||
|
public function index(ProductionOrderIndexRequest $request): JsonResponse
|
||||||
|
{
|
||||||
|
$result = $this->service->index($request->validated());
|
||||||
|
|
||||||
|
return ApiResponse::success($result, __('message.fetched'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 생산지시 상태별 통계
|
||||||
|
*/
|
||||||
|
public function stats(): JsonResponse
|
||||||
|
{
|
||||||
|
$stats = $this->service->stats();
|
||||||
|
|
||||||
|
return ApiResponse::success($stats, __('message.fetched'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 생산지시 상세 조회
|
||||||
|
*/
|
||||||
|
public function show(int $orderId): JsonResponse
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$detail = $this->service->show($orderId);
|
||||||
|
|
||||||
|
return ApiResponse::success($detail, __('message.fetched'));
|
||||||
|
} catch (\Illuminate\Database\Eloquent\ModelNotFoundException $e) {
|
||||||
|
return ApiResponse::error(__('error.order.not_found'), 404);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
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'));
|
||||||
|
}
|
||||||
|
}
|
||||||
147
app/Http/Controllers/Api/V1/QualityDocumentController.php
Normal file
147
app/Http/Controllers/Api/V1/QualityDocumentController.php
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api\V1;
|
||||||
|
|
||||||
|
use App\Helpers\ApiResponse;
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Http\Requests\Quality\QualityDocumentStoreRequest;
|
||||||
|
use App\Http\Requests\Quality\QualityDocumentUpdateRequest;
|
||||||
|
use App\Services\QualityDocumentService;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class QualityDocumentController extends Controller
|
||||||
|
{
|
||||||
|
public function __construct(private QualityDocumentService $service) {}
|
||||||
|
|
||||||
|
public function index(Request $request)
|
||||||
|
{
|
||||||
|
return ApiResponse::handle(function () use ($request) {
|
||||||
|
return $this->service->index($request->all());
|
||||||
|
}, __('message.fetched'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stats(Request $request)
|
||||||
|
{
|
||||||
|
return ApiResponse::handle(function () use ($request) {
|
||||||
|
return $this->service->stats($request->all());
|
||||||
|
}, __('message.fetched'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function calendar(Request $request)
|
||||||
|
{
|
||||||
|
return ApiResponse::handle(function () use ($request) {
|
||||||
|
return $this->service->calendar($request->all());
|
||||||
|
}, __('message.fetched'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function availableOrders(Request $request)
|
||||||
|
{
|
||||||
|
return ApiResponse::handle(function () use ($request) {
|
||||||
|
return $this->service->availableOrders($request->all());
|
||||||
|
}, __('message.fetched'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function show(int $id)
|
||||||
|
{
|
||||||
|
return ApiResponse::handle(function () use ($id) {
|
||||||
|
return $this->service->show($id);
|
||||||
|
}, __('message.fetched'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(QualityDocumentStoreRequest $request)
|
||||||
|
{
|
||||||
|
return ApiResponse::handle(function () use ($request) {
|
||||||
|
return $this->service->store($request->validated());
|
||||||
|
}, __('message.created'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(QualityDocumentUpdateRequest $request, int $id)
|
||||||
|
{
|
||||||
|
return ApiResponse::handle(function () use ($request, $id) {
|
||||||
|
return $this->service->update($id, $request->validated());
|
||||||
|
}, __('message.updated'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy(int $id)
|
||||||
|
{
|
||||||
|
return ApiResponse::handle(function () use ($id) {
|
||||||
|
$this->service->destroy($id);
|
||||||
|
|
||||||
|
return 'success';
|
||||||
|
}, __('message.deleted'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function complete(int $id)
|
||||||
|
{
|
||||||
|
return ApiResponse::handle(function () use ($id) {
|
||||||
|
return $this->service->complete($id);
|
||||||
|
}, __('message.updated'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function attachOrders(Request $request, int $id)
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'order_ids' => ['required', 'array', 'min:1'],
|
||||||
|
'order_ids.*' => ['required', 'integer'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
return ApiResponse::handle(function () use ($request, $id) {
|
||||||
|
return $this->service->attachOrders($id, $request->input('order_ids'));
|
||||||
|
}, __('message.updated'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function detachOrder(int $id, int $orderId)
|
||||||
|
{
|
||||||
|
return ApiResponse::handle(function () use ($id, $orderId) {
|
||||||
|
return $this->service->detachOrder($id, $orderId);
|
||||||
|
}, __('message.updated'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function inspectLocation(Request $request, int $id, int $locId)
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'post_width' => ['nullable', 'integer'],
|
||||||
|
'post_height' => ['nullable', 'integer'],
|
||||||
|
'change_reason' => ['nullable', 'string', 'max:500'],
|
||||||
|
'inspection_status' => ['nullable', 'string', 'in:pending,completed'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
return ApiResponse::handle(function () use ($request, $id, $locId) {
|
||||||
|
return $this->service->inspectLocation($id, $locId, $request->all());
|
||||||
|
}, __('message.updated'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function requestDocument(int $id)
|
||||||
|
{
|
||||||
|
return ApiResponse::handle(function () use ($id) {
|
||||||
|
return $this->service->requestDocument($id);
|
||||||
|
}, __('message.fetched'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function resultDocument(int $id)
|
||||||
|
{
|
||||||
|
return ApiResponse::handle(function () use ($id) {
|
||||||
|
return $this->service->resultDocument($id);
|
||||||
|
}, __('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 옵션 조회
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
use App\Helpers\ApiResponse;
|
use App\Helpers\ApiResponse;
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Http\Requests\V1\Stock\StoreStockAdjustmentRequest;
|
||||||
use App\Services\StockService;
|
use App\Services\StockService;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
@@ -71,4 +72,32 @@ public function statsByItemType(): JsonResponse
|
|||||||
|
|
||||||
return ApiResponse::success($stats, __('message.fetched'));
|
return ApiResponse::success($stats, __('message.fetched'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 재고 조정 이력 조회
|
||||||
|
*/
|
||||||
|
public function adjustments(int $id): JsonResponse
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$adjustments = $this->service->adjustments($id);
|
||||||
|
|
||||||
|
return ApiResponse::success($adjustments, __('message.fetched'));
|
||||||
|
} catch (\Illuminate\Database\Eloquent\ModelNotFoundException $e) {
|
||||||
|
return ApiResponse::error(__('error.stock.not_found'), 404);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 재고 조정 등록
|
||||||
|
*/
|
||||||
|
public function storeAdjustment(int $id, StoreStockAdjustmentRequest $request): JsonResponse
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$result = $this->service->createAdjustment($id, $request->validated());
|
||||||
|
|
||||||
|
return ApiResponse::success($result, __('message.created'));
|
||||||
|
} catch (\Illuminate\Database\Eloquent\ModelNotFoundException $e) {
|
||||||
|
return ApiResponse::error(__('error.stock.not_found'), 404);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,12 +10,33 @@
|
|||||||
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 App\Services\TenantSettingService;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
class TaxInvoiceController extends Controller
|
class TaxInvoiceController extends Controller
|
||||||
{
|
{
|
||||||
|
private const SUPPLIER_GROUP = 'supplier';
|
||||||
|
|
||||||
|
private const SUPPLIER_KEYS = [
|
||||||
|
'business_number',
|
||||||
|
'company_name',
|
||||||
|
'representative_name',
|
||||||
|
'address',
|
||||||
|
'business_type',
|
||||||
|
'business_item',
|
||||||
|
'contact_name',
|
||||||
|
'contact_phone',
|
||||||
|
'contact_email',
|
||||||
|
];
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private TaxInvoiceService $taxInvoiceService
|
private TaxInvoiceService $taxInvoiceService,
|
||||||
|
private JournalSyncService $journalSyncService,
|
||||||
|
private TenantSettingService $tenantSettingService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -23,12 +44,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 +54,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 +64,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 +74,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 +84,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 +96,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 +106,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 +116,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 +126,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 +136,121 @@ 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,
|
// 공급자 설정 (Supplier Settings)
|
||||||
message: __('message.fetched')
|
// =========================================================================
|
||||||
);
|
|
||||||
|
/**
|
||||||
|
* 공급자 설정 조회
|
||||||
|
*/
|
||||||
|
public function getSupplierSettings(): JsonResponse
|
||||||
|
{
|
||||||
|
return ApiResponse::handle(function () {
|
||||||
|
$settings = $this->tenantSettingService->getByGroup(self::SUPPLIER_GROUP);
|
||||||
|
|
||||||
|
$result = [];
|
||||||
|
foreach (self::SUPPLIER_KEYS as $key) {
|
||||||
|
$result[$key] = $settings[$key] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}, __('message.fetched'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 공급자 설정 저장
|
||||||
|
*/
|
||||||
|
public function saveSupplierSettings(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
return ApiResponse::handle(function () use ($request) {
|
||||||
|
$data = $request->only(self::SUPPLIER_KEYS);
|
||||||
|
|
||||||
|
$settings = [];
|
||||||
|
foreach ($data as $key => $value) {
|
||||||
|
if (in_array($key, self::SUPPLIER_KEYS)) {
|
||||||
|
$settings[$key] = $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->tenantSettingService->setMany(self::SUPPLIER_GROUP, $settings);
|
||||||
|
|
||||||
|
return $settings;
|
||||||
|
}, __('message.updated'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// 분개 (Journal Entries)
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 세금계산서 분개 조회
|
||||||
|
*/
|
||||||
|
public function getJournalEntries(int $id): JsonResponse
|
||||||
|
{
|
||||||
|
return ApiResponse::handle(function () use ($id) {
|
||||||
|
$sourceKey = "tax_invoice_{$id}";
|
||||||
|
$data = $this->journalSyncService->getForSource(
|
||||||
|
JournalEntry::SOURCE_TAX_INVOICE,
|
||||||
|
$sourceKey
|
||||||
|
);
|
||||||
|
|
||||||
|
return $data ?? ['rows' => []];
|
||||||
|
}, __('message.fetched'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 세금계산서 분개 저장/수정
|
||||||
|
*/
|
||||||
|
public function storeJournalEntries(Request $request, int $id): JsonResponse
|
||||||
|
{
|
||||||
|
return ApiResponse::handle(function () use ($request, $id) {
|
||||||
|
$validated = $request->validate([
|
||||||
|
'rows' => 'required|array|min:1',
|
||||||
|
'rows.*.side' => 'required|in:debit,credit',
|
||||||
|
'rows.*.account_subject' => 'required|string|max:20',
|
||||||
|
'rows.*.debit_amount' => 'required|integer|min:0',
|
||||||
|
'rows.*.credit_amount' => 'required|integer|min:0',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 세금계산서 정보 조회 (entry_date용)
|
||||||
|
$taxInvoice = $this->taxInvoiceService->show($id);
|
||||||
|
|
||||||
|
$rows = array_map(fn ($row) => [
|
||||||
|
'side' => $row['side'],
|
||||||
|
'account_code' => $row['account_subject'],
|
||||||
|
'debit_amount' => $row['debit_amount'],
|
||||||
|
'credit_amount' => $row['credit_amount'],
|
||||||
|
], $validated['rows']);
|
||||||
|
|
||||||
|
$sourceKey = "tax_invoice_{$id}";
|
||||||
|
|
||||||
|
return $this->journalSyncService->saveForSource(
|
||||||
|
JournalEntry::SOURCE_TAX_INVOICE,
|
||||||
|
$sourceKey,
|
||||||
|
$taxInvoice->issue_date?->format('Y-m-d') ?? now()->format('Y-m-d'),
|
||||||
|
"세금계산서 분개 (#{$id})",
|
||||||
|
$rows,
|
||||||
|
);
|
||||||
|
}, __('message.created'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 세금계산서 분개 삭제
|
||||||
|
*/
|
||||||
|
public function deleteJournalEntries(int $id): JsonResponse
|
||||||
|
{
|
||||||
|
return ApiResponse::handle(function () use ($id) {
|
||||||
|
$sourceKey = "tax_invoice_{$id}";
|
||||||
|
|
||||||
|
return $this->journalSyncService->deleteForSource(
|
||||||
|
JournalEntry::SOURCE_TAX_INVOICE,
|
||||||
|
$sourceKey
|
||||||
|
);
|
||||||
|
}, __('message.deleted'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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',
|
||||||
@@ -124,6 +125,13 @@ public function handle(Request $request, Closure $next)
|
|||||||
'api/v1/internal/exchange-token', // 내부 서버간 토큰 교환 (HMAC 인증 사용)
|
'api/v1/internal/exchange-token', // 내부 서버간 토큰 교환 (HMAC 인증 사용)
|
||||||
'api/v1/admin/fcm/*', // Admin FCM API (MNG에서 API Key만으로 접근)
|
'api/v1/admin/fcm/*', // Admin FCM API (MNG에서 API Key만으로 접근)
|
||||||
'api/v1/app/*', // 앱 버전 확인/다운로드 (API Key만 필요)
|
'api/v1/app/*', // 앱 버전 확인/다운로드 (API Key만 필요)
|
||||||
|
'api/v1/bending-items', // 절곡품 목록 (MNG에서 API Key만으로 접근)
|
||||||
|
'api/v1/bending-items/*', // 절곡품 상세/필터
|
||||||
|
'api/v1/guiderail-models', // 절곡품 모델 목록
|
||||||
|
'api/v1/guiderail-models/*', // 절곡품 모델 상세/필터
|
||||||
|
'api/v1/items/*/files', // 품목 파일 업로드/조회
|
||||||
|
'api/v1/files/*/view', // 파일 인라인 보기 (MNG 이미지 표시)
|
||||||
|
'api/v1/files/*/download', // 파일 다운로드
|
||||||
];
|
];
|
||||||
|
|
||||||
// 현재 라우트 확인 (경로 또는 이름)
|
// 현재 라우트 확인 (경로 또는 이름)
|
||||||
|
|||||||
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
27
app/Http/Requests/Api/V1/BendingItemIndexRequest.php
Normal file
27
app/Http/Requests/Api/V1/BendingItemIndexRequest.php
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests\Api\V1;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class BendingItemIndexRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'item_sep' => 'nullable|string|in:스크린,철재',
|
||||||
|
'item_bending' => 'nullable|string',
|
||||||
|
'material' => 'nullable|string',
|
||||||
|
'model_UA' => 'nullable|string|in:인정,비인정',
|
||||||
|
'model_name' => 'nullable|string',
|
||||||
|
'search' => 'nullable|string|max:100',
|
||||||
|
'page' => 'nullable|integer|min:1',
|
||||||
|
'size' => 'nullable|integer|min:1|max:200',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
47
app/Http/Requests/Api/V1/BendingItemStoreRequest.php
Normal file
47
app/Http/Requests/Api/V1/BendingItemStoreRequest.php
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests\Api\V1;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class BendingItemStoreRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'code' => 'required|string|max:100|unique:items,code',
|
||||||
|
'name' => 'required|string|max:200',
|
||||||
|
'unit' => 'nullable|string|max:20',
|
||||||
|
'item_name' => 'required|string|max:50',
|
||||||
|
'item_sep' => 'required|in:스크린,철재',
|
||||||
|
'item_bending' => 'required|string|max:50',
|
||||||
|
'material' => 'required|string|max:50',
|
||||||
|
'model_UA' => 'nullable|in:인정,비인정',
|
||||||
|
'item_spec' => 'nullable|string|max:50',
|
||||||
|
'model_name' => 'nullable|string|max:30',
|
||||||
|
'search_keyword' => 'nullable|string|max:100',
|
||||||
|
'rail_width' => 'nullable|integer',
|
||||||
|
'memo' => 'nullable|string|max:500',
|
||||||
|
'author' => 'nullable|string|max:50',
|
||||||
|
'registration_date' => 'nullable|date',
|
||||||
|
// 케이스 전용
|
||||||
|
'exit_direction' => 'nullable|string|max:30',
|
||||||
|
'front_bottom_width' => 'nullable|integer',
|
||||||
|
'box_width' => 'nullable|integer',
|
||||||
|
'box_height' => 'nullable|integer',
|
||||||
|
// 전개도
|
||||||
|
'bendingData' => 'nullable|array',
|
||||||
|
'bendingData.*.no' => 'required|integer',
|
||||||
|
'bendingData.*.input' => 'required|numeric',
|
||||||
|
'bendingData.*.rate' => 'nullable|string',
|
||||||
|
'bendingData.*.sum' => 'required|numeric',
|
||||||
|
'bendingData.*.color' => 'required|boolean',
|
||||||
|
'bendingData.*.aAngle' => 'required|boolean',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
46
app/Http/Requests/Api/V1/BendingItemUpdateRequest.php
Normal file
46
app/Http/Requests/Api/V1/BendingItemUpdateRequest.php
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests\Api\V1;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class BendingItemUpdateRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'code' => 'sometimes|string|max:100',
|
||||||
|
'name' => 'sometimes|string|max:200',
|
||||||
|
'item_name' => 'sometimes|string|max:50',
|
||||||
|
'item_sep' => 'sometimes|in:스크린,철재',
|
||||||
|
'item_bending' => 'sometimes|string|max:50',
|
||||||
|
'material' => 'sometimes|string|max:50',
|
||||||
|
'model_UA' => 'nullable|in:인정,비인정',
|
||||||
|
'item_spec' => 'nullable|string|max:50',
|
||||||
|
'model_name' => 'nullable|string|max:30',
|
||||||
|
'search_keyword' => 'nullable|string|max:100',
|
||||||
|
'rail_width' => 'nullable|integer',
|
||||||
|
'memo' => 'nullable|string|max:500',
|
||||||
|
'author' => 'nullable|string|max:50',
|
||||||
|
'registration_date' => 'nullable|date',
|
||||||
|
// 케이스 전용
|
||||||
|
'exit_direction' => 'nullable|string|max:30',
|
||||||
|
'front_bottom_width' => 'nullable|integer',
|
||||||
|
'box_width' => 'nullable|integer',
|
||||||
|
'box_height' => 'nullable|integer',
|
||||||
|
// 전개도
|
||||||
|
'bendingData' => 'nullable|array',
|
||||||
|
'bendingData.*.no' => 'required|integer',
|
||||||
|
'bendingData.*.input' => 'required|numeric',
|
||||||
|
'bendingData.*.rate' => 'nullable|string',
|
||||||
|
'bendingData.*.sum' => 'required|numeric',
|
||||||
|
'bendingData.*.color' => 'required|boolean',
|
||||||
|
'bendingData.*.aAngle' => 'required|boolean',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
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' => '유효하지 않은 프리셋입니다.',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,6 +28,9 @@ public function rules(): array
|
|||||||
'approvers.*.user_id' => 'required_with:approvers|integer|exists:users,id',
|
'approvers.*.user_id' => 'required_with:approvers|integer|exists:users,id',
|
||||||
'approvers.*.role' => 'nullable|string|max:50',
|
'approvers.*.role' => 'nullable|string|max:50',
|
||||||
|
|
||||||
|
// HTML 스냅샷
|
||||||
|
'rendered_html' => 'nullable|string',
|
||||||
|
|
||||||
// 문서 데이터 (EAV)
|
// 문서 데이터 (EAV)
|
||||||
'data' => 'nullable|array',
|
'data' => 'nullable|array',
|
||||||
'data.*.section_id' => 'nullable|integer',
|
'data.*.section_id' => 'nullable|integer',
|
||||||
|
|||||||
@@ -27,6 +27,9 @@ public function rules(): array
|
|||||||
'approvers.*.user_id' => 'required_with:approvers|integer|exists:users,id',
|
'approvers.*.user_id' => 'required_with:approvers|integer|exists:users,id',
|
||||||
'approvers.*.role' => 'nullable|string|max:50',
|
'approvers.*.role' => 'nullable|string|max:50',
|
||||||
|
|
||||||
|
// HTML 스냅샷
|
||||||
|
'rendered_html' => 'nullable|string',
|
||||||
|
|
||||||
// 문서 데이터 (EAV)
|
// 문서 데이터 (EAV)
|
||||||
'data' => 'nullable|array',
|
'data' => 'nullable|array',
|
||||||
'data.*.section_id' => 'nullable|integer',
|
'data.*.section_id' => 'nullable|integer',
|
||||||
|
|||||||
@@ -30,6 +30,9 @@ public function rules(): array
|
|||||||
'data.*.field_key' => 'required_with:data|string|max:100',
|
'data.*.field_key' => 'required_with:data|string|max:100',
|
||||||
'data.*.field_value' => 'nullable|string',
|
'data.*.field_value' => 'nullable|string',
|
||||||
|
|
||||||
|
// HTML 스냅샷
|
||||||
|
'rendered_html' => 'nullable|string',
|
||||||
|
|
||||||
// 첨부파일
|
// 첨부파일
|
||||||
'attachments' => 'nullable|array',
|
'attachments' => 'nullable|array',
|
||||||
'attachments.*.file_id' => 'required_with:attachments|integer|exists:files,id',
|
'attachments.*.file_id' => 'required_with:attachments|integer|exists:files,id',
|
||||||
|
|||||||
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'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,6 +16,13 @@ public function rules(): array
|
|||||||
return [
|
return [
|
||||||
'delivery_date' => 'nullable|date',
|
'delivery_date' => 'nullable|date',
|
||||||
'memo' => 'nullable|string',
|
'memo' => 'nullable|string',
|
||||||
|
'delivery_method_code' => 'nullable|string',
|
||||||
|
'options' => 'nullable|array',
|
||||||
|
'options.receiver' => 'nullable|string',
|
||||||
|
'options.receiver_contact' => 'nullable|string',
|
||||||
|
'options.shipping_address' => 'nullable|string',
|
||||||
|
'options.shipping_address_detail' => 'nullable|string',
|
||||||
|
'options.shipping_cost_code' => 'nullable|string',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ public function rules(): array
|
|||||||
return [
|
return [
|
||||||
// 기본 정보
|
// 기본 정보
|
||||||
'quote_id' => 'nullable|integer|exists:quotes,id',
|
'quote_id' => 'nullable|integer|exists:quotes,id',
|
||||||
'order_type_code' => ['nullable', Rule::in([Order::TYPE_ORDER, Order::TYPE_PURCHASE])],
|
'order_type_code' => ['nullable', Rule::in([Order::TYPE_ORDER, Order::TYPE_PURCHASE, Order::TYPE_STOCK])],
|
||||||
'status_code' => ['nullable', Rule::in([
|
'status_code' => ['nullable', Rule::in([
|
||||||
Order::STATUS_DRAFT,
|
Order::STATUS_DRAFT,
|
||||||
Order::STATUS_CONFIRMED,
|
Order::STATUS_CONFIRMED,
|
||||||
@@ -55,6 +55,18 @@ public function rules(): array
|
|||||||
'options.shipping_address' => 'nullable|string|max:500',
|
'options.shipping_address' => 'nullable|string|max:500',
|
||||||
'options.shipping_address_detail' => 'nullable|string|max:500',
|
'options.shipping_address_detail' => 'nullable|string|max:500',
|
||||||
'options.manager_name' => 'nullable|string|max:100',
|
'options.manager_name' => 'nullable|string|max:100',
|
||||||
|
'options.production_reason' => 'nullable|string|max:500',
|
||||||
|
'options.target_stock_qty' => 'nullable|numeric|min:0',
|
||||||
|
|
||||||
|
// 절곡품 LOT 정보 (STOCK 전용)
|
||||||
|
'options.bending_lot' => 'nullable|array',
|
||||||
|
'options.bending_lot.lot_number' => 'nullable|string|max:30',
|
||||||
|
'options.bending_lot.prod_code' => 'nullable|string|max:2',
|
||||||
|
'options.bending_lot.spec_code' => 'nullable|string|max:2',
|
||||||
|
'options.bending_lot.length_code' => 'nullable|string|max:2',
|
||||||
|
'options.bending_lot.raw_lot_no' => 'nullable|string|max:50',
|
||||||
|
'options.bending_lot.fabric_lot_no' => 'nullable|string|max:50',
|
||||||
|
'options.bending_lot.material' => 'nullable|string|max:50',
|
||||||
|
|
||||||
// 품목 배열
|
// 품목 배열
|
||||||
'items' => 'nullable|array',
|
'items' => 'nullable|array',
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ public function rules(): array
|
|||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
// 기본 정보 (order_no는 수정 불가)
|
// 기본 정보 (order_no는 수정 불가)
|
||||||
'order_type_code' => ['nullable', Rule::in([Order::TYPE_ORDER, Order::TYPE_PURCHASE])],
|
'order_type_code' => ['nullable', Rule::in([Order::TYPE_ORDER, Order::TYPE_PURCHASE, Order::TYPE_STOCK])],
|
||||||
'category_code' => 'nullable|string|max:50',
|
'category_code' => 'nullable|string|max:50',
|
||||||
|
|
||||||
// 거래처 정보
|
// 거래처 정보
|
||||||
@@ -49,6 +49,18 @@ public function rules(): array
|
|||||||
'options.shipping_address' => 'nullable|string|max:500',
|
'options.shipping_address' => 'nullable|string|max:500',
|
||||||
'options.shipping_address_detail' => 'nullable|string|max:500',
|
'options.shipping_address_detail' => 'nullable|string|max:500',
|
||||||
'options.manager_name' => 'nullable|string|max:100',
|
'options.manager_name' => 'nullable|string|max:100',
|
||||||
|
'options.production_reason' => 'nullable|string|max:500',
|
||||||
|
'options.target_stock_qty' => 'nullable|numeric|min:0',
|
||||||
|
|
||||||
|
// 절곡품 LOT 정보 (STOCK 전용)
|
||||||
|
'options.bending_lot' => 'nullable|array',
|
||||||
|
'options.bending_lot.lot_number' => 'nullable|string|max:30',
|
||||||
|
'options.bending_lot.prod_code' => 'nullable|string|max:2',
|
||||||
|
'options.bending_lot.spec_code' => 'nullable|string|max:2',
|
||||||
|
'options.bending_lot.length_code' => 'nullable|string|max:2',
|
||||||
|
'options.bending_lot.raw_lot_no' => 'nullable|string|max:50',
|
||||||
|
'options.bending_lot.fabric_lot_no' => 'nullable|string|max:50',
|
||||||
|
'options.bending_lot.material' => 'nullable|string|max:50',
|
||||||
|
|
||||||
// 품목 배열 (전체 교체)
|
// 품목 배열 (전체 교체)
|
||||||
'items' => 'nullable|array',
|
'items' => 'nullable|array',
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests\ProductionOrder;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class ProductionOrderIndexRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'search' => 'nullable|string|max:100',
|
||||||
|
'production_status' => 'nullable|in:waiting,in_production,completed',
|
||||||
|
'sort_by' => 'nullable|in:created_at,delivery_date,order_no',
|
||||||
|
'sort_dir' => 'nullable|in:asc,desc',
|
||||||
|
'page' => 'nullable|integer|min:1',
|
||||||
|
'per_page' => 'nullable|integer|min:1|max:100',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
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',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests\Quality;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class PerformanceReportConfirmRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'ids' => ['required', 'array', 'min:1'],
|
||||||
|
'ids.*' => ['required', 'integer', 'exists:performance_reports,id'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function messages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'ids.required' => __('validation.required', ['attribute' => '실적신고 ID']),
|
||||||
|
'ids.min' => __('validation.min.array', ['attribute' => '실적신고 ID', 'min' => 1]),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
30
app/Http/Requests/Quality/PerformanceReportMemoRequest.php
Normal file
30
app/Http/Requests/Quality/PerformanceReportMemoRequest.php
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests\Quality;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class PerformanceReportMemoRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'ids' => ['required', 'array', 'min:1'],
|
||||||
|
'ids.*' => ['required', 'integer', 'exists:performance_reports,id'],
|
||||||
|
'memo' => ['required', 'string', 'max:1000'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function messages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'ids.required' => __('validation.required', ['attribute' => '실적신고 ID']),
|
||||||
|
'memo.required' => __('validation.required', ['attribute' => '메모']),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
45
app/Http/Requests/Quality/QualityDocumentStoreRequest.php
Normal file
45
app/Http/Requests/Quality/QualityDocumentStoreRequest.php
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests\Quality;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class QualityDocumentStoreRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'site_name' => ['required', 'string', 'max:200'],
|
||||||
|
'client_id' => ['nullable', 'integer', 'exists:clients,id'],
|
||||||
|
'inspector_id' => ['nullable', 'integer', 'exists:users,id'],
|
||||||
|
'received_date' => ['nullable', 'date'],
|
||||||
|
'options' => ['nullable', 'array'],
|
||||||
|
'options.manager' => ['nullable', 'array'],
|
||||||
|
'options.manager.name' => ['nullable', 'string', 'max:50'],
|
||||||
|
'options.manager.phone' => ['nullable', 'string', 'max:30'],
|
||||||
|
'options.inspection' => ['nullable', 'array'],
|
||||||
|
'options.inspection.request_date' => ['nullable', 'date'],
|
||||||
|
'options.inspection.start_date' => ['nullable', 'date'],
|
||||||
|
'options.inspection.end_date' => ['nullable', 'date'],
|
||||||
|
'options.site_address' => ['nullable', 'array'],
|
||||||
|
'options.construction_site' => ['nullable', 'array'],
|
||||||
|
'options.material_distributor' => ['nullable', 'array'],
|
||||||
|
'options.contractor' => ['nullable', 'array'],
|
||||||
|
'options.supervisor' => ['nullable', 'array'],
|
||||||
|
'order_ids' => ['nullable', 'array'],
|
||||||
|
'order_ids.*' => ['integer', 'exists:orders,id'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function messages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'site_name.required' => __('validation.required', ['attribute' => '현장명']),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
44
app/Http/Requests/Quality/QualityDocumentUpdateRequest.php
Normal file
44
app/Http/Requests/Quality/QualityDocumentUpdateRequest.php
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests\Quality;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class QualityDocumentUpdateRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'site_name' => ['sometimes', 'string', 'max:200'],
|
||||||
|
'client_id' => ['nullable', 'integer', 'exists:clients,id'],
|
||||||
|
'inspector_id' => ['nullable', 'integer', 'exists:users,id'],
|
||||||
|
'received_date' => ['nullable', 'date'],
|
||||||
|
'options' => ['nullable', 'array'],
|
||||||
|
'options.manager' => ['nullable', 'array'],
|
||||||
|
'options.manager.name' => ['nullable', 'string', 'max:50'],
|
||||||
|
'options.manager.phone' => ['nullable', 'string', 'max:30'],
|
||||||
|
'options.inspection' => ['nullable', 'array'],
|
||||||
|
'options.inspection.request_date' => ['nullable', 'date'],
|
||||||
|
'options.inspection.start_date' => ['nullable', 'date'],
|
||||||
|
'options.inspection.end_date' => ['nullable', 'date'],
|
||||||
|
'options.site_address' => ['nullable', 'array'],
|
||||||
|
'options.construction_site' => ['nullable', 'array'],
|
||||||
|
'options.material_distributor' => ['nullable', 'array'],
|
||||||
|
'options.contractor' => ['nullable', 'array'],
|
||||||
|
'options.supervisor' => ['nullable', 'array'],
|
||||||
|
'order_ids' => ['nullable', 'array'],
|
||||||
|
'order_ids.*' => ['integer', 'exists:orders,id'],
|
||||||
|
'locations' => ['nullable', 'array'],
|
||||||
|
'locations.*.id' => ['required', 'integer'],
|
||||||
|
'locations.*.post_width' => ['nullable', 'integer'],
|
||||||
|
'locations.*.post_height' => ['nullable', 'integer'],
|
||||||
|
'locations.*.change_reason' => ['nullable', 'string', 'max:500'],
|
||||||
|
'locations.*.inspection_data' => ['nullable', 'array'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
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',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user