feat: 더미 데이터 시더 추가 및 회계 관련 마이그레이션

- DummyDataSeeder 및 개별 시더 추가 (Client, BadDebt, Deposit 등)
- payments.paid_at nullable 마이그레이션
- subscriptions 취소 컬럼 추가
- clients 테이블 bad_debt 컬럼 제거
- PlanController, ClientService 수정
- 불필요한 claudedocs, flow-test 파일 정리
This commit is contained in:
2025-12-24 08:54:52 +09:00
parent 71123128ff
commit 8686b199ee
30 changed files with 1278 additions and 2655 deletions

View File

@@ -1,6 +1,6 @@
# 논리적 데이터베이스 관계 문서 # 논리적 데이터베이스 관계 문서
> **자동 생성**: 2025-12-22 19:39:30 > **자동 생성**: 2025-12-23 22:25:44
> **소스**: Eloquent 모델 관계 분석 > **소스**: Eloquent 모델 관계 분석
## 📊 모델별 관계 현황 ## 📊 모델별 관계 현황
@@ -150,6 +150,10 @@ ### estimate_items
- **estimate()**: belongsTo → `estimates` - **estimate()**: belongsTo → `estimates`
### fcm_send_logs
**모델**: `App\Models\FcmSendLog`
### file_share_links ### file_share_links
**모델**: `App\Models\FileShareLink` **모델**: `App\Models\FileShareLink`
@@ -287,6 +291,8 @@ ### clients
- **clientGroup()**: belongsTo → `client_groups` - **clientGroup()**: belongsTo → `client_groups`
- **orders()**: hasMany → `orders` - **orders()**: hasMany → `orders`
- **badDebts()**: hasMany → `bad_debts`
- **activeBadDebts()**: hasMany → `bad_debts`
### client_groups ### client_groups
**모델**: `App\Models\Orders\ClientGroup` **모델**: `App\Models\Orders\ClientGroup`
@@ -489,6 +495,20 @@ ### barobill_settings
- **creator()**: belongsTo → `users` - **creator()**: belongsTo → `users`
- **updater()**: belongsTo → `users` - **updater()**: belongsTo → `users`
### bills
**모델**: `App\Models\Tenants\Bill`
- **client()**: belongsTo → `clients`
- **bankAccount()**: belongsTo → `bank_accounts`
- **creator()**: belongsTo → `users`
- **installments()**: hasMany → `bill_installments`
### bill_installments
**모델**: `App\Models\Tenants\BillInstallment`
- **bill()**: belongsTo → `bills`
- **creator()**: belongsTo → `users`
### cards ### cards
**모델**: `App\Models\Tenants\Card` **모델**: `App\Models\Tenants\Card`

View File

@@ -3,6 +3,7 @@
namespace App\Exceptions; namespace App\Exceptions;
use Illuminate\Auth\AuthenticationException; use Illuminate\Auth\AuthenticationException;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler; use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Http;
use Illuminate\Validation\ValidationException; use Illuminate\Validation\ValidationException;
@@ -114,7 +115,7 @@ public function render($request, Throwable $exception)
], 403); ], 403);
} }
// 404 Not Found // 404 Not Found - 라우트 없음
if ($exception instanceof NotFoundHttpException) { if ($exception instanceof NotFoundHttpException) {
return response()->json([ return response()->json([
'success' => false, 'success' => false,
@@ -123,6 +124,15 @@ public function render($request, Throwable $exception)
], 404); ], 404);
} }
// 404 Not Found - 모델 없음 (findOrFail 등)
if ($exception instanceof ModelNotFoundException) {
return response()->json([
'success' => false,
'message' => '데이터를 찾을 수 없습니다',
'data' => null,
], 404);
}
// 405 Method Not Allowed // 405 Method Not Allowed
if ($exception instanceof MethodNotAllowedHttpException) { if ($exception instanceof MethodNotAllowedHttpException) {
return response()->json([ return response()->json([

View File

@@ -2,11 +2,11 @@
namespace App\Http\Controllers\Api\V1; namespace App\Http\Controllers\Api\V1;
use App\Helpers\ApiResponse;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Http\Requests\V1\Plan\PlanIndexRequest; use App\Http\Requests\V1\Plan\PlanIndexRequest;
use App\Http\Requests\V1\Plan\PlanStoreRequest; use App\Http\Requests\V1\Plan\PlanStoreRequest;
use App\Http\Requests\V1\Plan\PlanUpdateRequest; use App\Http\Requests\V1\Plan\PlanUpdateRequest;
use App\Helpers\ApiResponse;
use App\Services\PlanService; use App\Services\PlanService;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
@@ -21,9 +21,10 @@ public function __construct(
*/ */
public function index(PlanIndexRequest $request): JsonResponse public function index(PlanIndexRequest $request): JsonResponse
{ {
$result = $this->planService->index($request->validated()); return ApiResponse::handle(
fn () => $this->planService->index($request->validated()),
return ApiResponse::handle('message.fetched', $result); __('message.fetched')
);
} }
/** /**
@@ -31,9 +32,10 @@ public function index(PlanIndexRequest $request): JsonResponse
*/ */
public function active(): JsonResponse public function active(): JsonResponse
{ {
$result = $this->planService->active(); return ApiResponse::handle(
fn () => $this->planService->active(),
return ApiResponse::handle('message.fetched', $result); __('message.fetched')
);
} }
/** /**
@@ -41,9 +43,10 @@ public function active(): JsonResponse
*/ */
public function store(PlanStoreRequest $request): JsonResponse public function store(PlanStoreRequest $request): JsonResponse
{ {
$result = $this->planService->store($request->validated()); return ApiResponse::handle(
fn () => $this->planService->store($request->validated()),
return ApiResponse::handle('message.created', $result, 201); __('message.created')
);
} }
/** /**
@@ -51,9 +54,10 @@ public function store(PlanStoreRequest $request): JsonResponse
*/ */
public function show(int $id): JsonResponse public function show(int $id): JsonResponse
{ {
$result = $this->planService->show($id); return ApiResponse::handle(
fn () => $this->planService->show($id),
return ApiResponse::handle('message.fetched', $result); __('message.fetched')
);
} }
/** /**
@@ -61,9 +65,10 @@ public function show(int $id): JsonResponse
*/ */
public function update(PlanUpdateRequest $request, int $id): JsonResponse public function update(PlanUpdateRequest $request, int $id): JsonResponse
{ {
$result = $this->planService->update($id, $request->validated()); return ApiResponse::handle(
fn () => $this->planService->update($id, $request->validated()),
return ApiResponse::handle('message.updated', $result); __('message.updated')
);
} }
/** /**
@@ -71,9 +76,10 @@ public function update(PlanUpdateRequest $request, int $id): JsonResponse
*/ */
public function destroy(int $id): JsonResponse public function destroy(int $id): JsonResponse
{ {
$this->planService->destroy($id); return ApiResponse::handle(
fn () => $this->planService->destroy($id),
return ApiResponse::handle('message.deleted'); __('message.deleted')
);
} }
/** /**
@@ -81,8 +87,9 @@ public function destroy(int $id): JsonResponse
*/ */
public function toggle(int $id): JsonResponse public function toggle(int $id): JsonResponse
{ {
$result = $this->planService->toggle($id); return ApiResponse::handle(
fn () => $this->planService->toggle($id),
return ApiResponse::handle('message.updated', $result); __('message.updated')
);
} }
} }

View File

@@ -2,9 +2,11 @@
namespace App\Models\Orders; namespace App\Models\Orders;
use App\Models\BadDebts\BadDebt;
use App\Traits\BelongsToTenant; use App\Traits\BelongsToTenant;
use App\Traits\ModelTrait; use App\Traits\ModelTrait;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Client extends Model class Client extends Model
{ {
@@ -32,11 +34,6 @@ class Client extends Model
'tax_amount', 'tax_amount',
'tax_start_date', 'tax_start_date',
'tax_end_date', 'tax_end_date',
'bad_debt',
'bad_debt_amount',
'bad_debt_receive_date',
'bad_debt_end_date',
'bad_debt_progress',
'memo', 'memo',
'is_active', 'is_active',
'client_type', 'client_type',
@@ -51,10 +48,6 @@ class Client extends Model
'tax_amount' => 'decimal:2', 'tax_amount' => 'decimal:2',
'tax_start_date' => 'date', 'tax_start_date' => 'date',
'tax_end_date' => 'date', 'tax_end_date' => 'date',
'bad_debt' => 'boolean',
'bad_debt_amount' => 'decimal:2',
'bad_debt_receive_date' => 'date',
'bad_debt_end_date' => 'date',
]; ];
protected $hidden = [ protected $hidden = [
@@ -73,6 +66,20 @@ public function orders()
return $this->hasMany(Order::class, 'client_id'); return $this->hasMany(Order::class, 'client_id');
} }
// 악성채권 관계
public function badDebts(): HasMany
{
return $this->hasMany(BadDebt::class);
}
// 활성 악성채권 관계 (추심중, 법적조치)
public function activeBadDebts(): HasMany
{
return $this->hasMany(BadDebt::class)
->whereIn('status', [BadDebt::STATUS_COLLECTING, BadDebt::STATUS_LEGAL_ACTION])
->where('is_active', true);
}
// 스코프 // 스코프
public function scopeActive($query) public function scopeActive($query)
{ {

View File

@@ -2,7 +2,11 @@
namespace App\Services; namespace App\Services;
use App\Models\BadDebts\BadDebt;
use App\Models\Orders\Client; use App\Models\Orders\Client;
use App\Models\Tenants\Deposit;
use App\Models\Tenants\Sale;
use Illuminate\Support\Facades\DB;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
@@ -34,7 +38,51 @@ public function index(array $params)
$query->orderBy('client_code')->orderBy('id'); $query->orderBy('client_code')->orderBy('id');
return $query->paginate($size, ['*'], 'page', $page); $paginator = $query->paginate($size, ['*'], 'page', $page);
// 미수금 계산: 매출 합계 - 입금 합계
$clientIds = $paginator->pluck('id')->toArray();
if (! empty($clientIds)) {
// 거래처별 매출 합계
$salesByClient = Sale::where('tenant_id', $tenantId)
->whereIn('client_id', $clientIds)
->whereNull('deleted_at')
->groupBy('client_id')
->select('client_id', DB::raw('SUM(total_amount) as total_sales'))
->pluck('total_sales', 'client_id');
// 거래처별 입금 합계
$depositsByClient = Deposit::where('tenant_id', $tenantId)
->whereIn('client_id', $clientIds)
->whereNull('deleted_at')
->groupBy('client_id')
->select('client_id', DB::raw('SUM(amount) as total_deposits'))
->pluck('total_deposits', 'client_id');
// 거래처별 활성 악성채권 합계 (추심중, 법적조치)
$badDebtsByClient = BadDebt::where('tenant_id', $tenantId)
->whereIn('client_id', $clientIds)
->whereIn('status', [BadDebt::STATUS_COLLECTING, BadDebt::STATUS_LEGAL_ACTION])
->where('is_active', true)
->whereNull('deleted_at')
->groupBy('client_id')
->select('client_id', DB::raw('SUM(debt_amount) as total_bad_debt'))
->pluck('total_bad_debt', 'client_id');
// 각 거래처에 미수금/악성채권 정보 추가
$paginator->getCollection()->transform(function ($client) use ($salesByClient, $depositsByClient, $badDebtsByClient) {
$totalSales = $salesByClient[$client->id] ?? 0;
$totalDeposits = $depositsByClient[$client->id] ?? 0;
$client->outstanding_amount = max(0, $totalSales - $totalDeposits);
$client->bad_debt_total = $badDebtsByClient[$client->id] ?? 0;
$client->has_bad_debt = ($badDebtsByClient[$client->id] ?? 0) > 0;
return $client;
});
}
return $paginator;
} }
/** 단건 */ /** 단건 */
@@ -46,6 +94,30 @@ public function show(int $id)
throw new NotFoundHttpException(__('error.not_found')); throw new NotFoundHttpException(__('error.not_found'));
} }
// 미수금 계산
$totalSales = Sale::where('tenant_id', $tenantId)
->where('client_id', $id)
->whereNull('deleted_at')
->sum('total_amount');
$totalDeposits = Deposit::where('tenant_id', $tenantId)
->where('client_id', $id)
->whereNull('deleted_at')
->sum('amount');
$client->outstanding_amount = max(0, $totalSales - $totalDeposits);
// 악성채권 정보
$badDebtTotal = BadDebt::where('tenant_id', $tenantId)
->where('client_id', $id)
->whereIn('status', [BadDebt::STATUS_COLLECTING, BadDebt::STATUS_LEGAL_ACTION])
->where('is_active', true)
->whereNull('deleted_at')
->sum('debt_amount');
$client->bad_debt_total = $badDebtTotal;
$client->has_bad_debt = $badDebtTotal > 0;
return $client; return $client;
} }

View File

@@ -1,85 +0,0 @@
{
"name": "인증 플로우 테스트",
"description": "로그인, 프로필 조회, 토큰 갱신, 로그아웃 플로우를 테스트합니다.",
"version": "1.0",
"config": {
"apiKey": "42Jfwc6EaRQ04GNRmLR5kzJp5UudSOzGGqjmdk1a",
"baseUrl": "https://api.sam.kr/api/v1",
"timeout": 30000,
"stopOnFailure": true
},
"variables": {
"user_id": "codebridgex",
"user_pwd": "code1234"
},
"steps": [
{
"id": "login",
"name": "로그인",
"method": "POST",
"endpoint": "/login",
"body": {
"user_id": "{{variables.user_id}}",
"user_pwd": "{{variables.user_pwd}}"
},
"extract": {
"accessToken": "$.access_token",
"refreshToken": "$.refresh_token"
},
"expect": {
"status": [200],
"jsonPath": {
"$.access_token": "@isString"
}
}
},
{
"id": "get_profile",
"name": "프로필 조회",
"method": "GET",
"endpoint": "/users/me",
"headers": {
"Authorization": "Bearer {{login.accessToken}}"
},
"dependsOn": ["login"],
"expect": {
"status": [200],
"jsonPath": {
"$.success": true
}
}
},
{
"id": "refresh_token",
"name": "토큰 갱신",
"method": "POST",
"endpoint": "/refresh",
"body": {
"refresh_token": "{{login.refreshToken}}"
},
"dependsOn": ["get_profile"],
"extract": {
"newToken": "$.access_token"
},
"expect": {
"status": [200],
"jsonPath": {
"$.access_token": "@isString"
}
}
},
{
"id": "logout",
"name": "로그아웃",
"method": "POST",
"endpoint": "/logout",
"headers": {
"Authorization": "Bearer {{refresh_token.newToken}}"
},
"dependsOn": ["refresh_token"],
"expect": {
"status": [200, 204]
}
}
]
}

View File

@@ -1,215 +0,0 @@
{
"name": "Client API CRUD 테스트",
"description": "거래처(Client) API 전체 CRUD 테스트 - 생성, 조회, 수정, 토글, 삭제 포함. business_no, business_type, business_item 신규 필드 검증 포함.",
"version": "1.0",
"config": {
"baseUrl": "https://api.sam.kr/api/v1",
"apiKey": "{{$env.FLOW_TESTER_API_KEY}}",
"timeout": 30000,
"stopOnFailure": true
},
"variables": {
"user_id": "{{$env.FLOW_TESTER_USER_ID}}",
"user_pwd": "{{$env.FLOW_TESTER_USER_PWD}}",
"test_client_code": "TEST_CLIENT_{{$timestamp}}"
},
"steps": [
{
"id": "login",
"name": "1. 로그인 - 토큰 획득",
"method": "POST",
"endpoint": "/login",
"body": {
"user_id": "{{variables.user_id}}",
"user_pwd": "{{variables.user_pwd}}"
},
"expect": {
"status": [200],
"jsonPath": {
"$.access_token": "@isString"
}
},
"extract": {
"token": "$.access_token"
}
},
{
"id": "create_client",
"name": "2. 거래처 생성 (신규 필드 포함)",
"method": "POST",
"endpoint": "/clients",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"body": {
"client_code": "{{variables.test_client_code}}",
"name": "테스트 거래처",
"contact_person": "홍길동",
"phone": "02-1234-5678",
"email": "test@example.com",
"address": "서울시 강남구 테헤란로 123",
"business_no": "123-45-67890",
"business_type": "제조업",
"business_item": "전자부품",
"is_active": "Y"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true,
"$.data.id": "@isNumber",
"$.data.client_code": "{{variables.test_client_code}}",
"$.data.name": "테스트 거래처",
"$.data.business_no": "123-45-67890",
"$.data.business_type": "제조업",
"$.data.business_item": "전자부품"
}
},
"extract": {
"client_id": "$.data.id"
}
},
{
"id": "list_clients",
"name": "3. 거래처 목록 조회",
"method": "GET",
"endpoint": "/clients?page=1&size=20&q=테스트",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true,
"$.data.data": "@isArray",
"$.data.current_page": 1
}
}
},
{
"id": "show_client",
"name": "4. 거래처 단건 조회",
"method": "GET",
"endpoint": "/clients/{{create_client.client_id}}",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true,
"$.data.id": "{{create_client.client_id}}",
"$.data.client_code": "{{variables.test_client_code}}",
"$.data.business_no": "123-45-67890",
"$.data.business_type": "제조업",
"$.data.business_item": "전자부품"
}
}
},
{
"id": "update_client",
"name": "5. 거래처 수정 (신규 필드 변경)",
"method": "PUT",
"endpoint": "/clients/{{create_client.client_id}}",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"body": {
"name": "테스트 거래처 (수정됨)",
"contact_person": "김철수",
"business_no": "987-65-43210",
"business_type": "도소매업",
"business_item": "IT솔루션"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true,
"$.data.name": "테스트 거래처 (수정됨)",
"$.data.contact_person": "김철수",
"$.data.business_no": "987-65-43210",
"$.data.business_type": "도소매업",
"$.data.business_item": "IT솔루션"
}
}
},
{
"id": "toggle_client",
"name": "6. 거래처 활성/비활성 토글 (N으로)",
"method": "PATCH",
"endpoint": "/clients/{{create_client.client_id}}/toggle",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true,
"$.data.is_active": false
}
}
},
{
"id": "toggle_client_back",
"name": "7. 거래처 토글 복원 (Y로)",
"method": "PATCH",
"endpoint": "/clients/{{create_client.client_id}}/toggle",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true,
"$.data.is_active": true
}
}
},
{
"id": "list_active_only",
"name": "8. 활성 거래처만 조회",
"method": "GET",
"endpoint": "/clients?only_active=1",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true,
"$.data.data": "@isArray"
}
}
},
{
"id": "delete_client",
"name": "9. 거래처 삭제",
"method": "DELETE",
"endpoint": "/clients/{{create_client.client_id}}",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true
}
}
},
{
"id": "verify_deleted",
"name": "10. 삭제 확인 (404 예상)",
"method": "GET",
"endpoint": "/clients/{{create_client.client_id}}",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"expect": {
"status": [404],
"jsonPath": {
"$.success": false
}
}
}
]
}

View File

@@ -1,287 +0,0 @@
{
"name": "품목 삭제 API 테스트",
"description": "품목 삭제 시 참조 무결성 체크 및 soft delete 동작을 테스트합니다.",
"version": "1.0",
"config": {
"apiKey": "42Jfwc6EaRQ04GNRmLR5kzJp5UudSOzGGqjmdk1a",
"baseUrl": "https://api.sam.kr/api/v1",
"timeout": 30000,
"stopOnFailure": false
},
"variables": {
"user_id": "codebridgex",
"user_pwd": "code1234",
"testProductCode": "TEST-DEL-001",
"testProductName": "삭제 테스트용 품목",
"testBomParentCode": "TEST-BOM-PARENT-001",
"testBomParentName": "BOM 부모 품목"
},
"steps": [
{
"id": "login",
"name": "로그인",
"description": "API 테스트를 위한 로그인",
"method": "POST",
"endpoint": "/login",
"body": {
"user_id": "{{variables.user_id}}",
"user_pwd": "{{variables.user_pwd}}"
},
"extract": {
"accessToken": "$.access_token",
"refreshToken": "$.refresh_token"
},
"expect": {
"status": [200],
"jsonPath": {
"$.access_token": "@isString"
}
}
},
{
"id": "create_product_for_delete",
"name": "삭제 테스트용 품목 생성",
"description": "단순 삭제 테스트용 품목 생성 (BOM에 미사용)",
"method": "POST",
"endpoint": "/items",
"headers": {
"Authorization": "Bearer {{login.accessToken}}"
},
"body": {
"code": "{{variables.testProductCode}}",
"name": "{{variables.testProductName}}",
"unit": "EA",
"product_type": "FG",
"is_active": true
},
"dependsOn": ["login"],
"extract": {
"createdProductId": "$.data.id",
"createdProductCode": "$.data.code"
},
"expect": {
"status": [200, 201],
"jsonPath": {
"$.success": true,
"$.data.id": "@isNumber"
}
}
},
{
"id": "get_items_list",
"name": "품목 목록 조회",
"description": "생성된 품목이 목록에 있는지 확인",
"method": "GET",
"endpoint": "/items?type=FG&search={{create_product_for_delete.createdProductCode}}",
"headers": {
"Authorization": "Bearer {{login.accessToken}}"
},
"dependsOn": ["create_product_for_delete"],
"expect": {
"status": [200],
"jsonPath": {
"$.success": true
}
}
},
{
"id": "delete_product_success",
"name": "품목 삭제 (정상)",
"description": "BOM에서 사용되지 않는 품목 삭제 - 성공해야 함",
"method": "DELETE",
"endpoint": "/items/{{create_product_for_delete.createdProductId}}",
"headers": {
"Authorization": "Bearer {{login.accessToken}}"
},
"dependsOn": ["get_items_list"],
"expect": {
"status": [200],
"jsonPath": {
"$.success": true
}
}
},
{
"id": "verify_deleted_not_in_list",
"name": "삭제된 품목 목록 미포함 확인",
"description": "삭제된 품목이 기본 목록에서 제외되는지 확인",
"method": "GET",
"endpoint": "/items?type=FG&search={{create_product_for_delete.createdProductCode}}",
"headers": {
"Authorization": "Bearer {{login.accessToken}}"
},
"dependsOn": ["delete_product_success"],
"expect": {
"status": [200],
"jsonPath": {
"$.success": true
}
},
"note": "data.total이 0이어야 함 (삭제된 품목 미포함)"
},
{
"id": "delete_already_deleted_item",
"name": "이미 삭제된 품목 재삭제 시도",
"description": "soft delete된 품목을 다시 삭제하면 404 반환해야 함",
"method": "DELETE",
"endpoint": "/items/{{create_product_for_delete.createdProductId}}",
"headers": {
"Authorization": "Bearer {{login.accessToken}}"
},
"dependsOn": ["verify_deleted_not_in_list"],
"expect": {
"status": [404],
"jsonPath": {
"$.success": false
}
}
},
{
"id": "create_bom_parent",
"name": "BOM 부모 품목 생성",
"description": "BOM 테스트를 위한 부모 품목 생성",
"method": "POST",
"endpoint": "/items",
"headers": {
"Authorization": "Bearer {{login.accessToken}}"
},
"body": {
"code": "{{variables.testBomParentCode}}",
"name": "{{variables.testBomParentName}}",
"unit": "EA",
"product_type": "FG",
"is_active": true
},
"dependsOn": ["login"],
"extract": {
"bomParentId": "$.data.id"
},
"expect": {
"status": [200, 201],
"jsonPath": {
"$.success": true
}
}
},
{
"id": "create_bom_child",
"name": "BOM 자식 품목 생성",
"description": "BOM 구성품으로 사용될 품목 생성",
"method": "POST",
"endpoint": "/items",
"headers": {
"Authorization": "Bearer {{login.accessToken}}"
},
"body": {
"code": "TEST-BOM-CHILD-001",
"name": "BOM 자식 품목",
"unit": "EA",
"product_type": "PT",
"is_active": true
},
"dependsOn": ["create_bom_parent"],
"extract": {
"bomChildId": "$.data.id"
},
"expect": {
"status": [200, 201],
"jsonPath": {
"$.success": true
}
}
},
{
"id": "add_bom_component",
"name": "BOM 구성품 추가",
"description": "부모 품목에 자식 품목을 BOM으로 등록",
"method": "POST",
"endpoint": "/items/{{create_bom_parent.bomParentId}}/bom",
"headers": {
"Authorization": "Bearer {{login.accessToken}}"
},
"body": {
"items": [
{
"ref_type": "PRODUCT",
"ref_id": "{{create_bom_child.bomChildId}}",
"quantity": 2,
"sort_order": 1
}
]
},
"dependsOn": ["create_bom_child"],
"expect": {
"status": [200, 201],
"jsonPath": {
"$.success": true
}
}
},
{
"id": "delete_bom_used_item_fail",
"name": "BOM 사용 중인 품목 삭제 시도",
"description": "다른 BOM에서 구성품으로 사용 중인 품목 삭제 - 400 에러 반환해야 함",
"method": "DELETE",
"endpoint": "/items/{{create_bom_child.bomChildId}}",
"headers": {
"Authorization": "Bearer {{login.accessToken}}"
},
"dependsOn": ["add_bom_component"],
"expect": {
"status": [400],
"jsonPath": {
"$.success": false,
"$.message": "@contains:BOM"
}
}
},
{
"id": "cleanup_bom",
"name": "BOM 구성품 제거",
"description": "테스트 정리 - BOM 구성품 제거",
"method": "DELETE",
"endpoint": "/items/{{create_bom_parent.bomParentId}}/bom",
"headers": {
"Authorization": "Bearer {{login.accessToken}}"
},
"dependsOn": ["delete_bom_used_item_fail"],
"expect": {
"status": [200, 204]
}
},
{
"id": "delete_bom_child_after_cleanup",
"name": "BOM 해제 후 자식 품목 삭제",
"description": "BOM에서 제거된 품목은 삭제 가능해야 함",
"method": "DELETE",
"endpoint": "/items/{{create_bom_child.bomChildId}}",
"headers": {
"Authorization": "Bearer {{login.accessToken}}"
},
"dependsOn": ["cleanup_bom"],
"expect": {
"status": [200],
"jsonPath": {
"$.success": true
}
}
},
{
"id": "cleanup_bom_parent",
"name": "BOM 부모 품목 삭제",
"description": "테스트 정리 - 부모 품목 삭제",
"method": "DELETE",
"endpoint": "/items/{{create_bom_parent.bomParentId}}",
"headers": {
"Authorization": "Bearer {{login.accessToken}}"
},
"dependsOn": ["delete_bom_child_after_cleanup"],
"expect": {
"status": [200],
"jsonPath": {
"$.success": true
}
}
}
]
}

View File

@@ -1,157 +0,0 @@
{
"name": "품목기준관리 통합 테스트",
"description": "품목기준관리 API의 전체 CRUD 플로우를 테스트합니다.",
"version": "1.0",
"config": {
"apiKey": "42Jfwc6EaRQ04GNRmLR5kzJp5UudSOzGGqjmdk1a",
"baseUrl": "https://api.sam.kr/api/v1",
"timeout": 30000,
"stopOnFailure": true
},
"variables": {
"user_id": "codebridgex",
"user_pwd": "code1234",
"testItemCode": "TEST-ITEM-{{$timestamp}}",
"testItemName": "테스트 품목",
"testItemSpec": "100x100x10",
"updatedItemName": "수정된 테스트 품목",
"updatedItemSpec": "200x200x20"
},
"steps": [
{
"id": "login",
"name": "로그인",
"method": "POST",
"endpoint": "/login",
"body": {
"user_id": "{{variables.user_id}}",
"user_pwd": "{{variables.user_pwd}}"
},
"extract": {
"accessToken": "$.access_token"
},
"expect": {
"status": [200],
"jsonPath": {
"$.access_token": "@isString"
}
}
},
{
"id": "create_item",
"name": "품목 생성",
"method": "POST",
"endpoint": "/item-master-data",
"headers": {
"Authorization": "Bearer {{login.accessToken}}"
},
"body": {
"item_code": "{{variables.testItemCode}}",
"item_name": "{{variables.testItemName}}",
"item_spec": "{{variables.testItemSpec}}",
"item_type": "PRODUCT",
"unit": "EA",
"is_active": true
},
"dependsOn": ["login"],
"extract": {
"createdItemId": "$.data.id",
"createdItemCode": "$.data.item_code"
},
"expect": {
"status": [201],
"jsonPath": {
"$.data.id": "@exists"
}
}
},
{
"id": "get_item",
"name": "품목 단건 조회",
"method": "GET",
"endpoint": "/item-master-data/{{create_item.createdItemId}}",
"headers": {
"Authorization": "Bearer {{login.accessToken}}"
},
"dependsOn": ["create_item"],
"expect": {
"status": [200],
"jsonPath": {
"$.data.id": "@exists"
}
}
},
{
"id": "list_items",
"name": "품목 목록 조회",
"method": "GET",
"endpoint": "/item-master-data",
"headers": {
"Authorization": "Bearer {{login.accessToken}}"
},
"dependsOn": ["create_item"],
"expect": {
"status": [200],
"jsonPath": {
"$.data": "@isArray"
}
}
},
{
"id": "update_item",
"name": "품목 수정",
"method": "PUT",
"endpoint": "/item-master-data/{{create_item.createdItemId}}",
"headers": {
"Authorization": "Bearer {{login.accessToken}}"
},
"body": {
"item_name": "{{variables.updatedItemName}}",
"item_spec": "{{variables.updatedItemSpec}}"
},
"dependsOn": ["get_item"],
"expect": {
"status": [200]
}
},
{
"id": "verify_update",
"name": "수정 확인",
"method": "GET",
"endpoint": "/item-master-data/{{create_item.createdItemId}}",
"headers": {
"Authorization": "Bearer {{login.accessToken}}"
},
"dependsOn": ["update_item"],
"expect": {
"status": [200]
}
},
{
"id": "delete_item",
"name": "품목 삭제",
"method": "DELETE",
"endpoint": "/item-master-data/{{create_item.createdItemId}}",
"headers": {
"Authorization": "Bearer {{login.accessToken}}"
},
"dependsOn": ["verify_update"],
"expect": {
"status": [200]
}
},
{
"id": "verify_delete",
"name": "삭제 확인",
"method": "GET",
"endpoint": "/item-master-data/{{create_item.createdItemId}}",
"headers": {
"Authorization": "Bearer {{login.accessToken}}"
},
"dependsOn": ["delete_item"],
"expect": {
"status": [404]
}
}
]
}

View File

@@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* 결제 대기 상태(pending)에서는 paid_at이 null이어야 하므로 nullable로 변경
*/
public function up(): void
{
Schema::table('payments', function (Blueprint $table) {
$table->datetime('paid_at')->nullable()->change();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('payments', function (Blueprint $table) {
$table->datetime('paid_at')->nullable(false)->change();
});
}
};

View File

@@ -0,0 +1,31 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* Subscription 취소 시 필요한 컬럼 추가
*/
public function up(): void
{
Schema::table('subscriptions', function (Blueprint $table) {
$table->timestamp('cancelled_at')->nullable()->after('ended_at')->comment('취소 일시');
$table->string('cancel_reason', 500)->nullable()->after('cancelled_at')->comment('취소 사유');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('subscriptions', function (Blueprint $table) {
$table->dropColumn(['cancelled_at', 'cancel_reason']);
});
}
};

View File

@@ -0,0 +1,44 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
/**
* clients 테이블에서 bad_debt 관련 컬럼 제거
*
* 악성채권 정보는 이제 bad_debts 테이블에서 관리됨
* clients.bad_debts() 관계를 통해 조회
*/
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('clients', function (Blueprint $table) {
$table->dropColumn([
'bad_debt',
'bad_debt_amount',
'bad_debt_receive_date',
'bad_debt_end_date',
'bad_debt_progress',
]);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('clients', function (Blueprint $table) {
$table->boolean('bad_debt')->default(false)->after('tax_end_date')->comment('악성채권 여부');
$table->decimal('bad_debt_amount', 15, 2)->nullable()->after('bad_debt')->comment('악성채권 금액');
$table->date('bad_debt_receive_date')->nullable()->after('bad_debt_amount')->comment('악성채권 발생일');
$table->date('bad_debt_end_date')->nullable()->after('bad_debt_receive_date')->comment('악성채권 종료일');
$table->string('bad_debt_progress', 20)->nullable()->after('bad_debt_end_date')->comment('악성채권 진행상황');
});
}
};

View File

@@ -0,0 +1,100 @@
<?php
namespace Database\Seeders\Dummy;
use App\Models\BadDebts\BadDebt;
use App\Models\Orders\Client;
use Database\Seeders\DummyDataSeeder;
use Illuminate\Database\Seeder;
class DummyBadDebtSeeder extends Seeder
{
public function run(): void
{
$tenantId = DummyDataSeeder::TENANT_ID;
$userId = DummyDataSeeder::USER_ID;
// SALES 또는 BOTH 타입의 거래처 조회
$clients = Client::where('tenant_id', $tenantId)
->whereIn('client_type', ['SALES', 'BOTH'])
->get();
if ($clients->isEmpty()) {
$this->command->warn(' ⚠ clients 데이터가 없습니다. DummyClientSeeder를 먼저 실행하세요.');
return;
}
// 상태별 분포
$statuses = [
'collecting' => 6, // 추심중
'legal_action' => 4, // 법적조치
'recovered' => 5, // 회수완료
'bad_debt' => 3, // 대손처리
];
// 금액 범위 (채권 금액)
$amounts = [
'small' => [500000, 3000000], // 50만~300만
'medium' => [3000000, 15000000], // 300만~1500만
'large' => [15000000, 50000000], // 1500만~5000만
];
$count = 0;
$year = 2025;
$clientIndex = 0;
foreach ($statuses as $status => $qty) {
for ($i = 0; $i < $qty; $i++) {
$client = $clients[$clientIndex % $clients->count()];
$clientIndex++;
// 발생월 (1~10월 사이 랜덤)
$month = rand(1, 10);
$day = rand(1, 28);
$occurredAt = sprintf('%04d-%02d-%02d', $year, $month, $day);
// 연체일수 계산 (발생일로부터 현재까지)
$occurredDate = new \DateTime($occurredAt);
$now = new \DateTime('2025-12-23');
$overdueDays = $occurredDate->diff($now)->days;
// 금액 결정 (분포에 따라)
$rand = rand(1, 100);
if ($rand <= 40) {
$amount = rand($amounts['small'][0], $amounts['small'][1]);
} elseif ($rand <= 80) {
$amount = rand($amounts['medium'][0], $amounts['medium'][1]);
} else {
$amount = rand($amounts['large'][0], $amounts['large'][1]);
}
// 종료일 (회수완료/대손처리인 경우만)
$closedAt = null;
if (in_array($status, ['recovered', 'bad_debt'])) {
$closedMonth = rand($month + 1, 12);
$closedDay = rand(1, 28);
$closedAt = sprintf('%04d-%02d-%02d', $year, $closedMonth, $closedDay);
}
BadDebt::create([
'tenant_id' => $tenantId,
'client_id' => $client->id,
'debt_amount' => $amount,
'status' => $status,
'overdue_days' => $overdueDays,
'assigned_user_id' => $userId,
'occurred_at' => $occurredAt,
'closed_at' => $closedAt,
'is_active' => true,
'options' => null,
'created_by' => $userId,
]);
$count++;
}
}
$this->command->info(' ✓ bad_debts: ' . $count . '건 생성');
$this->command->info(' - collecting: 6건, legal_action: 4건, recovered: 5건, bad_debt: 3건');
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace Database\Seeders\Dummy;
use App\Models\Tenants\BankAccount;
use Database\Seeders\DummyDataSeeder;
use Illuminate\Database\Seeder;
class DummyBankAccountSeeder extends Seeder
{
public function run(): void
{
$tenantId = DummyDataSeeder::TENANT_ID;
$userId = DummyDataSeeder::USER_ID;
$accounts = [
['bank_code' => '004', 'bank_name' => 'KB국민은행', 'account_number' => '123-45-6789012', 'account_holder' => '프론트테스트', 'account_name' => '운영계좌', 'is_primary' => true],
['bank_code' => '088', 'bank_name' => '신한은행', 'account_number' => '110-123-456789', 'account_holder' => '프론트테스트', 'account_name' => '급여계좌', 'is_primary' => false],
['bank_code' => '020', 'bank_name' => '우리은행', 'account_number' => '1002-123-456789', 'account_holder' => '프론트테스트', 'account_name' => '예비계좌', 'is_primary' => false],
['bank_code' => '081', 'bank_name' => '하나은행', 'account_number' => '123-456789-12345','account_holder' => '프론트테스트', 'account_name' => '법인카드', 'is_primary' => false],
['bank_code' => '011', 'bank_name' => 'NH농협은행', 'account_number' => '351-1234-5678-12','account_holder' => '프론트테스트', 'account_name' => '비상금', 'is_primary' => false],
];
foreach ($accounts as $account) {
BankAccount::create([
'tenant_id' => $tenantId,
'bank_code' => $account['bank_code'],
'bank_name' => $account['bank_name'],
'account_number' => $account['account_number'],
'account_holder' => $account['account_holder'],
'account_name' => $account['account_name'],
'status' => 'active',
'is_primary' => $account['is_primary'],
'created_by' => $userId,
]);
}
$this->command->info(' ✓ bank_accounts: ' . count($accounts) . '건 생성');
}
}

View File

@@ -0,0 +1,37 @@
<?php
namespace Database\Seeders\Dummy;
use App\Models\Orders\ClientGroup;
use Database\Seeders\DummyDataSeeder;
use Illuminate\Database\Seeder;
class DummyClientGroupSeeder extends Seeder
{
public function run(): void
{
$tenantId = DummyDataSeeder::TENANT_ID;
$userId = DummyDataSeeder::USER_ID;
$groups = [
['group_code' => 'VIP', 'group_name' => 'VIP 고객', 'price_rate' => 0.95],
['group_code' => 'GOLD', 'group_name' => '골드 고객', 'price_rate' => 0.97],
['group_code' => 'SILVER', 'group_name' => '실버 고객', 'price_rate' => 0.98],
['group_code' => 'NORMAL', 'group_name' => '일반 고객', 'price_rate' => 1.00],
['group_code' => 'NEW', 'group_name' => '신규 고객', 'price_rate' => 1.00],
];
foreach ($groups as $group) {
ClientGroup::create([
'tenant_id' => $tenantId,
'group_code' => $group['group_code'],
'group_name' => $group['group_name'],
'price_rate' => $group['price_rate'],
'is_active' => true,
'created_by' => $userId,
]);
}
$this->command->info(' ✓ client_groups: ' . count($groups) . '건 생성');
}
}

View File

@@ -0,0 +1,94 @@
<?php
namespace Database\Seeders\Dummy;
use App\Models\Orders\Client;
use App\Models\Orders\ClientGroup;
use Database\Seeders\DummyDataSeeder;
use Illuminate\Database\Seeder;
class DummyClientSeeder extends Seeder
{
public function run(): void
{
$tenantId = DummyDataSeeder::TENANT_ID;
$userId = DummyDataSeeder::USER_ID;
// 그룹 ID 조회
$groups = ClientGroup::where('tenant_id', $tenantId)
->pluck('id', 'group_code')
->toArray();
// 매출처 (SALES) - 10개
$salesClients = [
['code' => 'S001', 'name' => '삼성전자', 'business_no' => '124-81-00998', 'group' => 'VIP', 'contact' => '김철수', 'phone' => '02-1234-5678', 'email' => 'kim@samsung.com'],
['code' => 'S002', 'name' => 'LG전자', 'business_no' => '107-86-14075', 'group' => 'VIP', 'contact' => '이영희', 'phone' => '02-2345-6789', 'email' => 'lee@lg.com'],
['code' => 'S003', 'name' => 'SK하이닉스', 'business_no' => '204-81-17169', 'group' => 'GOLD', 'contact' => '박민수', 'phone' => '031-123-4567', 'email' => 'park@skhynix.com'],
['code' => 'S004', 'name' => '현대자동차', 'business_no' => '101-81-05765', 'group' => 'GOLD', 'contact' => '정은지', 'phone' => '02-3456-7890', 'email' => 'jung@hyundai.com'],
['code' => 'S005', 'name' => '네이버', 'business_no' => '220-81-62517', 'group' => 'GOLD', 'contact' => '최준호', 'phone' => '031-234-5678', 'email' => 'choi@naver.com'],
['code' => 'S006', 'name' => '카카오', 'business_no' => '120-87-65763', 'group' => 'SILVER', 'contact' => '강미래', 'phone' => '02-4567-8901', 'email' => 'kang@kakao.com'],
['code' => 'S007', 'name' => '쿠팡', 'business_no' => '120-88-00767', 'group' => 'SILVER', 'contact' => '임도현', 'phone' => '02-5678-9012', 'email' => 'lim@coupang.com'],
['code' => 'S008', 'name' => '토스', 'business_no' => '120-87-83139', 'group' => 'NORMAL', 'contact' => '윤서연', 'phone' => '02-6789-0123', 'email' => 'yoon@toss.im'],
['code' => 'S009', 'name' => '배달의민족', 'business_no' => '220-87-93847', 'group' => 'NORMAL', 'contact' => '한지민', 'phone' => '02-7890-1234', 'email' => 'han@woowahan.com'],
['code' => 'S010', 'name' => '당근마켓', 'business_no' => '815-87-01234', 'group' => 'NEW', 'contact' => '오태양', 'phone' => '02-8901-2345', 'email' => 'oh@daangn.com'],
];
// 매입처 (PURCHASE) - 7개
$purchaseClients = [
['code' => 'P001', 'name' => '한화솔루션', 'business_no' => '138-81-00610', 'group' => null, 'contact' => '김재원', 'phone' => '02-1111-2222', 'email' => 'kim@hanwha.com'],
['code' => 'P002', 'name' => '포스코', 'business_no' => '506-81-08754', 'group' => null, 'contact' => '이현석', 'phone' => '054-111-2222', 'email' => 'lee@posco.com'],
['code' => 'P003', 'name' => '롯데케미칼', 'business_no' => '301-81-07123', 'group' => null, 'contact' => '박서준', 'phone' => '02-2222-3333', 'email' => 'park@lottechem.com'],
['code' => 'P004', 'name' => 'GS칼텍스', 'business_no' => '104-81-23858', 'group' => null, 'contact' => '정해인', 'phone' => '02-3333-4444', 'email' => 'jung@gscaltex.com'],
['code' => 'P005', 'name' => '대한항공', 'business_no' => '110-81-14794', 'group' => null, 'contact' => '송민호', 'phone' => '02-4444-5555', 'email' => 'song@koreanair.com'],
['code' => 'P006', 'name' => '현대제철', 'business_no' => '130-81-12345', 'group' => null, 'contact' => '강동원', 'phone' => '032-555-6666', 'email' => 'kang@hyundaisteel.com'],
['code' => 'P007', 'name' => 'SK이노베이션', 'business_no' => '110-81-67890', 'group' => null, 'contact' => '유재석', 'phone' => '02-6666-7777', 'email' => 'yoo@skinnovation.com'],
];
// 매출매입처 (BOTH) - 3개
$bothClients = [
['code' => 'B001', 'name' => '두산에너빌리티', 'business_no' => '124-81-08628', 'group' => 'GOLD', 'contact' => '조인성', 'phone' => '02-7777-8888', 'email' => 'cho@doosan.com'],
['code' => 'B002', 'name' => 'CJ대한통운', 'business_no' => '104-81-39849', 'group' => 'SILVER', 'contact' => '공유', 'phone' => '02-8888-9999', 'email' => 'gong@cjlogistics.com'],
['code' => 'B003', 'name' => '삼성SDS', 'business_no' => '124-81-34567', 'group' => 'VIP', 'contact' => '이정재', 'phone' => '02-9999-0000', 'email' => 'lee@samsungsds.com'],
];
$count = 0;
// 매출처 생성
foreach ($salesClients as $client) {
$this->createClient($client, 'SALES', $tenantId, $userId, $groups);
$count++;
}
// 매입처 생성
foreach ($purchaseClients as $client) {
$this->createClient($client, 'PURCHASE', $tenantId, $userId, $groups);
$count++;
}
// 매출매입처 생성
foreach ($bothClients as $client) {
$this->createClient($client, 'BOTH', $tenantId, $userId, $groups);
$count++;
}
$this->command->info(' ✓ clients: ' . $count . '건 생성');
}
private function createClient(array $data, string $type, int $tenantId, int $userId, array $groups): void
{
Client::create([
'tenant_id' => $tenantId,
'client_group_id' => $data['group'] ? ($groups[$data['group']] ?? null) : null,
'client_code' => $data['code'],
'name' => $data['name'],
'client_type' => $type,
'contact_person' => $data['contact'],
'phone' => $data['phone'],
'email' => $data['email'],
'business_no' => $data['business_no'],
'business_type' => '제조업',
'business_item' => '전자제품',
'is_active' => true,
]);
}
}

View File

@@ -0,0 +1,94 @@
<?php
namespace Database\Seeders\Dummy;
use App\Models\Orders\Client;
use App\Models\Tenants\BankAccount;
use App\Models\Tenants\Deposit;
use Database\Seeders\DummyDataSeeder;
use Illuminate\Database\Seeder;
class DummyDepositSeeder extends Seeder
{
public function run(): void
{
$tenantId = DummyDataSeeder::TENANT_ID;
$userId = DummyDataSeeder::USER_ID;
// 거래처 매핑 (SALES, BOTH만)
$clients = Client::where('tenant_id', $tenantId)
->whereIn('client_type', ['SALES', 'BOTH'])
->get()
->keyBy('name');
// 은행계좌 (대표계좌 우선)
$bankAccounts = BankAccount::where('tenant_id', $tenantId)
->where('status', 'active')
->orderByDesc('is_primary')
->get();
$primaryBankId = $bankAccounts->first()?->id;
// 결제수단 분포: transfer(70%), card(15%), cash(10%), check(5%)
$methods = array_merge(
array_fill(0, 14, 'transfer'),
array_fill(0, 3, 'card'),
array_fill(0, 2, 'cash'),
array_fill(0, 1, 'check')
);
// 거래처 순환 목록
$clientNames = [
'삼성전자', 'LG전자', 'SK하이닉스', '현대자동차', '네이버',
'카카오', '쿠팡', '토스', '배달의민족', '당근마켓',
'두산에너빌리티', 'CJ대한통운', '삼성SDS',
];
// 금액 범위
$amounts = [
'small' => [1000000, 5000000], // 30%
'medium' => [5000000, 30000000], // 50%
'large' => [30000000, 100000000], // 20%
];
$count = 0;
$year = 2025;
// 월별 5건씩, 총 60건
for ($month = 1; $month <= 12; $month++) {
$daysInMonth = cal_days_in_month(CAL_GREGORIAN, $month, $year);
for ($i = 0; $i < 5; $i++) {
$day = rand(1, $daysInMonth);
$clientName = $clientNames[($month * 5 + $i) % count($clientNames)];
$client = $clients->get($clientName);
// 금액 결정 (분포에 따라)
$rand = rand(1, 100);
if ($rand <= 30) {
$amount = rand($amounts['small'][0], $amounts['small'][1]);
} elseif ($rand <= 80) {
$amount = rand($amounts['medium'][0], $amounts['medium'][1]);
} else {
$amount = rand($amounts['large'][0], $amounts['large'][1]);
}
Deposit::create([
'tenant_id' => $tenantId,
'deposit_date' => sprintf('%04d-%02d-%02d', $year, $month, $day),
'client_id' => $client?->id,
'client_name' => $client ? null : $clientName,
'bank_account_id' => $primaryBankId,
'amount' => $amount,
'payment_method' => $methods[array_rand($methods)],
'description' => $clientName . ' 입금',
'created_by' => $userId,
]);
$count++;
}
}
$this->command->info(' ✓ deposits: ' . $count . '건 생성');
}
}

View File

@@ -0,0 +1,219 @@
<?php
namespace Database\Seeders\Dummy;
use App\Models\Tenants\Payment;
use App\Models\Tenants\Plan;
use App\Models\Tenants\Subscription;
use Database\Seeders\DummyDataSeeder;
use Illuminate\Database\Seeder;
class DummyPaymentSeeder extends Seeder
{
public function run(): void
{
$tenantId = DummyDataSeeder::TENANT_ID;
$userId = DummyDataSeeder::USER_ID;
// 1. 요금제 생성 (없으면)
$plans = $this->createPlans($userId);
// 2. 구독 생성
$subscription = $this->createSubscription($tenantId, $plans['standard']->id, $userId);
// 3. 결제 내역 생성
$paymentCount = $this->createPayments($subscription, $userId);
$this->command->info(' ✓ plans: '.count($plans).'건 생성');
$this->command->info(' ✓ subscriptions: 1건 생성');
$this->command->info(' ✓ payments: '.$paymentCount.'건 생성');
}
/**
* 요금제 생성
*/
private function createPlans(int $userId): array
{
$plansData = [
'free' => [
'name' => '무료 체험',
'code' => 'FREE',
'description' => '14일 무료 체험',
'price' => 0,
'billing_cycle' => Plan::BILLING_MONTHLY,
'features' => ['basic_features', 'limited_users'],
'is_active' => true,
],
'starter' => [
'name' => '스타터',
'code' => 'STARTER',
'description' => '소규모 팀을 위한 기본 플랜',
'price' => 29000,
'billing_cycle' => Plan::BILLING_MONTHLY,
'features' => ['basic_features', 'email_support', 'up_to_5_users'],
'is_active' => true,
],
'standard' => [
'name' => '스탠다드',
'code' => 'STANDARD',
'description' => '성장하는 팀을 위한 표준 플랜',
'price' => 79000,
'billing_cycle' => Plan::BILLING_MONTHLY,
'features' => ['all_features', 'priority_support', 'up_to_20_users', 'api_access'],
'is_active' => true,
],
'professional' => [
'name' => '프로페셔널',
'code' => 'PRO',
'description' => '전문가 팀을 위한 고급 플랜',
'price' => 149000,
'billing_cycle' => Plan::BILLING_MONTHLY,
'features' => ['all_features', 'dedicated_support', 'unlimited_users', 'api_access', 'custom_integrations'],
'is_active' => true,
],
'enterprise' => [
'name' => '엔터프라이즈',
'code' => 'ENTERPRISE',
'description' => '대기업 맞춤형 플랜',
'price' => 499000,
'billing_cycle' => Plan::BILLING_MONTHLY,
'features' => ['all_features', 'dedicated_support', 'unlimited_users', 'api_access', 'custom_integrations', 'sla', 'on_premise'],
'is_active' => true,
],
'yearly_standard' => [
'name' => '스탠다드 (연간)',
'code' => 'STANDARD_YEARLY',
'description' => '연간 결제 시 20% 할인',
'price' => 758400, // 79000 * 12 * 0.8
'billing_cycle' => Plan::BILLING_YEARLY,
'features' => ['all_features', 'priority_support', 'up_to_20_users', 'api_access'],
'is_active' => true,
],
];
$plans = [];
foreach ($plansData as $key => $data) {
$plans[$key] = Plan::firstOrCreate(
['code' => $data['code']],
array_merge($data, ['created_by' => $userId])
);
}
return $plans;
}
/**
* 구독 생성
*/
private function createSubscription(int $tenantId, int $planId, int $userId): Subscription
{
// 기존 활성 구독이 있으면 반환
$existing = Subscription::where('tenant_id', $tenantId)
->where('status', Subscription::STATUS_ACTIVE)
->first();
if ($existing) {
return $existing;
}
// 새 구독 생성 (12개월 전부터 시작)
return Subscription::create([
'tenant_id' => $tenantId,
'plan_id' => $planId,
'started_at' => now()->subMonths(12)->startOfMonth(),
'ended_at' => now()->addMonths(1)->endOfMonth(),
'status' => Subscription::STATUS_ACTIVE,
'created_by' => $userId,
]);
}
/**
* 결제 내역 생성
*/
private function createPayments(Subscription $subscription, int $userId): int
{
// 이미 결제 내역이 있으면 스킵
if ($subscription->payments()->count() > 0) {
return 0;
}
$plan = $subscription->plan;
$paymentMethods = [
Payment::METHOD_CARD,
Payment::METHOD_CARD,
Payment::METHOD_CARD, // 카드 60%
Payment::METHOD_BANK,
Payment::METHOD_BANK, // 계좌이체 30%
Payment::METHOD_VIRTUAL, // 가상계좌 10%
];
$statuses = [
Payment::STATUS_COMPLETED,
Payment::STATUS_COMPLETED,
Payment::STATUS_COMPLETED,
Payment::STATUS_COMPLETED,
Payment::STATUS_COMPLETED,
Payment::STATUS_COMPLETED,
Payment::STATUS_COMPLETED,
Payment::STATUS_COMPLETED, // 완료 80%
Payment::STATUS_CANCELLED, // 취소 10%
Payment::STATUS_REFUNDED, // 환불 10%
];
$count = 0;
// 12개월치 결제 내역 생성
for ($i = 12; $i >= 0; $i--) {
$paymentDate = now()->subMonths($i)->startOfMonth();
// 이번 달은 대기 상태로
$status = $i === 0
? Payment::STATUS_PENDING
: $statuses[array_rand($statuses)];
// 금액 변동 (할인, 프로모션 등)
$baseAmount = $plan->price;
$amount = match (true) {
$i >= 10 => $baseAmount * 0.5, // 첫 3개월 50% 할인
$i >= 6 => $baseAmount * 0.8, // 다음 4개월 20% 할인
default => $baseAmount, // 이후 정가
};
// 취소/환불은 금액 0
if (in_array($status, [Payment::STATUS_CANCELLED, Payment::STATUS_REFUNDED])) {
$amount = $baseAmount;
}
$paidAt = $status === Payment::STATUS_COMPLETED
? $paymentDate->copy()->addDays(rand(1, 5))
: null;
$transactionId = $status === Payment::STATUS_COMPLETED
? 'TXN'.$paymentDate->format('Ymd').str_pad(rand(1, 9999), 4, '0', STR_PAD_LEFT)
: null;
$memo = match ($status) {
Payment::STATUS_CANCELLED => '고객 요청에 의한 취소',
Payment::STATUS_REFUNDED => '서비스 불만족으로 인한 환불',
Payment::STATUS_PENDING => '결제 대기 중',
default => null,
};
Payment::create([
'subscription_id' => $subscription->id,
'amount' => $amount,
'payment_method' => $paymentMethods[array_rand($paymentMethods)],
'transaction_id' => $transactionId,
'paid_at' => $paidAt,
'status' => $status,
'memo' => $memo,
'created_by' => $userId,
'created_at' => $paymentDate,
]);
$count++;
}
return $count;
}
}

View File

@@ -0,0 +1,107 @@
<?php
namespace Database\Seeders\Dummy;
use App\Models\Popups\Popup;
use Database\Seeders\DummyDataSeeder;
use Illuminate\Database\Seeder;
class DummyPopupSeeder extends Seeder
{
public function run(): void
{
$tenantId = DummyDataSeeder::TENANT_ID;
$userId = DummyDataSeeder::USER_ID;
$popups = [
[
'target_type' => 'all',
'target_id' => null,
'title' => '시스템 점검 안내',
'content' => '<p>안녕하세요.</p><p>2025년 1월 15일(수) 02:00 ~ 06:00 동안 시스템 점검이 예정되어 있습니다.</p><p>점검 시간 동안 서비스 이용이 제한될 수 있으니 양해 부탁드립니다.</p>',
'status' => 'active',
'started_at' => now()->subDays(5),
'ended_at' => now()->addDays(10),
],
[
'target_type' => 'all',
'target_id' => null,
'title' => '신규 기능 업데이트 안내',
'content' => '<p>새로운 기능이 추가되었습니다!</p><ul><li>대시보드 개선</li><li>보고서 내보내기 기능</li><li>알림 설정 강화</li></ul><p>자세한 내용은 도움말을 확인해 주세요.</p>',
'status' => 'active',
'started_at' => now()->subDays(3),
'ended_at' => now()->addDays(30),
],
[
'target_type' => 'all',
'target_id' => null,
'title' => '연말 휴무 안내',
'content' => '<p>2024년 연말 휴무 일정을 안내드립니다.</p><p><strong>휴무 기간</strong>: 12월 30일(월) ~ 1월 1일(수)</p><p>새해 복 많이 받으세요!</p>',
'status' => 'inactive',
'started_at' => now()->subMonth(),
'ended_at' => now()->subDays(20),
],
[
'target_type' => 'department',
'target_id' => 1,
'title' => '부서 회의 안내',
'content' => '<p>이번 주 금요일 오후 2시에 정기 회의가 있습니다.</p><p><strong>장소</strong>: 3층 회의실</p><p><strong>안건</strong>: 1분기 실적 검토</p>',
'status' => 'active',
'started_at' => now(),
'ended_at' => now()->addDays(7),
],
[
'target_type' => 'all',
'target_id' => null,
'title' => '보안 업데이트 필수 안내',
'content' => '<p><strong>중요!</strong></p><p>보안 강화를 위해 비밀번호 변경이 필요합니다.</p><p>최근 3개월 이내 비밀번호를 변경하지 않으신 분은 마이페이지에서 변경해 주세요.</p>',
'status' => 'active',
'started_at' => now()->subDays(1),
'ended_at' => now()->addDays(14),
],
[
'target_type' => 'all',
'target_id' => null,
'title' => '서비스 이용약관 변경 안내',
'content' => '<p>서비스 이용약관이 2025년 2월 1일부터 변경됩니다.</p><p>주요 변경 사항:</p><ul><li>개인정보 처리방침 개정</li><li>서비스 이용 조건 명확화</li></ul><p>변경된 약관은 공지사항에서 확인하실 수 있습니다.</p>',
'status' => 'active',
'started_at' => now(),
'ended_at' => now()->addDays(45),
],
[
'target_type' => 'department',
'target_id' => 2,
'title' => '영업팀 워크샵 안내',
'content' => '<p>영업팀 상반기 워크샵이 예정되어 있습니다.</p><p><strong>일시</strong>: 2025년 2월 15일(토)</p><p><strong>장소</strong>: 추후 공지</p><p>많은 참여 바랍니다!</p>',
'status' => 'active',
'started_at' => now()->addDays(5),
'ended_at' => now()->addDays(50),
],
[
'target_type' => 'all',
'target_id' => null,
'title' => '모바일 앱 출시 안내',
'content' => '<p>SAM 모바일 앱이 출시되었습니다!</p><p>앱스토어와 구글플레이에서 "SAM"을 검색해 주세요.</p><p>모바일에서도 편리하게 업무를 처리하세요.</p>',
'status' => 'inactive',
'started_at' => now()->subMonths(2),
'ended_at' => now()->subMonth(),
],
];
foreach ($popups as $popup) {
Popup::create([
'tenant_id' => $tenantId,
'target_type' => $popup['target_type'],
'target_id' => $popup['target_id'],
'title' => $popup['title'],
'content' => $popup['content'],
'status' => $popup['status'],
'started_at' => $popup['started_at'],
'ended_at' => $popup['ended_at'],
'created_by' => $userId,
]);
}
$this->command->info(' ✓ popups: ' . count($popups) . '건 생성');
}
}

View File

@@ -0,0 +1,91 @@
<?php
namespace Database\Seeders\Dummy;
use App\Models\Orders\Client;
use App\Models\Tenants\Purchase;
use Database\Seeders\DummyDataSeeder;
use Illuminate\Database\Seeder;
class DummyPurchaseSeeder extends Seeder
{
public function run(): void
{
$tenantId = DummyDataSeeder::TENANT_ID;
$userId = DummyDataSeeder::USER_ID;
// 거래처 매핑 (PURCHASE, BOTH만)
$clients = Client::where('tenant_id', $tenantId)
->whereIn('client_type', ['PURCHASE', 'BOTH'])
->get()
->keyBy('name');
$clientNames = [
'한화솔루션', '포스코', '롯데케미칼', 'GS칼텍스', '대한항공',
'현대제철', 'SK이노베이션', 'CJ대한통운', '두산에너빌리티',
];
$amounts = [
'small' => [1000000, 5000000],
'medium' => [5000000, 30000000],
'large' => [30000000, 80000000],
];
// 월별 건수: 1~10월 5~6건, 11월 7건, 12월 6건 = 70건
$monthlyCount = [5, 5, 6, 5, 6, 6, 6, 6, 6, 6, 7, 6];
$count = 0;
$year = 2025;
for ($month = 1; $month <= 12; $month++) {
$daysInMonth = cal_days_in_month(CAL_GREGORIAN, $month, $year);
$purchaseCount = $monthlyCount[$month - 1];
for ($i = 0; $i < $purchaseCount; $i++) {
$day = (int) (($i + 1) * $daysInMonth / ($purchaseCount + 1));
$day = max(1, min($day, $daysInMonth));
$clientName = $clientNames[($count) % count($clientNames)];
$client = $clients->get($clientName);
$rand = rand(1, 100);
if ($rand <= 30) {
$supply = rand($amounts['small'][0], $amounts['small'][1]);
} elseif ($rand <= 80) {
$supply = rand($amounts['medium'][0], $amounts['medium'][1]);
} else {
$supply = rand($amounts['large'][0], $amounts['large'][1]);
}
// 상태 결정
if ($month <= 10) {
$status = 'confirmed';
} elseif ($month == 11) {
$status = $i < 4 ? 'confirmed' : 'draft';
} else {
$status = $i < 1 ? 'confirmed' : 'draft';
}
$tax = round($supply * 0.1);
$total = $supply + $tax;
Purchase::create([
'tenant_id' => $tenantId,
'purchase_number' => sprintf('PUR-%04d%02d-%04d', $year, $month, $i + 1),
'purchase_date' => sprintf('%04d-%02d-%02d', $year, $month, $day),
'client_id' => $client->id,
'supply_amount' => $supply,
'tax_amount' => $tax,
'total_amount' => $total,
'description' => $clientName . ' 매입',
'status' => $status,
'created_by' => $userId,
]);
$count++;
}
}
$this->command->info(' ✓ purchases: ' . $count . '건 생성');
}
}

View File

@@ -0,0 +1,93 @@
<?php
namespace Database\Seeders\Dummy;
use App\Models\Orders\Client;
use App\Models\Tenants\Sale;
use Database\Seeders\DummyDataSeeder;
use Illuminate\Database\Seeder;
class DummySaleSeeder extends Seeder
{
public function run(): void
{
$tenantId = DummyDataSeeder::TENANT_ID;
$userId = DummyDataSeeder::USER_ID;
// 거래처 매핑 (SALES, BOTH만)
$clients = Client::where('tenant_id', $tenantId)
->whereIn('client_type', ['SALES', 'BOTH'])
->get()
->keyBy('name');
$clientNames = [
'삼성전자', 'LG전자', 'SK하이닉스', '현대자동차', '네이버',
'카카오', '쿠팡', '토스', '배달의민족', '당근마켓',
'두산에너빌리티', 'CJ대한통운', '삼성SDS',
];
$amounts = [
'small' => [1000000, 5000000],
'medium' => [5000000, 30000000],
'large' => [30000000, 100000000],
];
// 월별 건수: 1~10월 6~7건, 11월 8건, 12월 7건 = 80건
$monthlyCount = [6, 6, 7, 6, 7, 6, 7, 6, 7, 7, 8, 7];
$count = 0;
$year = 2025;
for ($month = 1; $month <= 12; $month++) {
$daysInMonth = cal_days_in_month(CAL_GREGORIAN, $month, $year);
$salesCount = $monthlyCount[$month - 1];
for ($i = 0; $i < $salesCount; $i++) {
$day = (int) (($i + 1) * $daysInMonth / ($salesCount + 1));
$day = max(1, min($day, $daysInMonth));
$clientName = $clientNames[($count) % count($clientNames)];
$client = $clients->get($clientName);
// 금액 결정
$rand = rand(1, 100);
if ($rand <= 30) {
$supply = rand($amounts['small'][0], $amounts['small'][1]);
} elseif ($rand <= 80) {
$supply = rand($amounts['medium'][0], $amounts['medium'][1]);
} else {
$supply = rand($amounts['large'][0], $amounts['large'][1]);
}
// 상태 결정: 1~10월 invoiced/confirmed, 11~12월 draft 포함
if ($month <= 10) {
$status = rand(0, 1) ? 'invoiced' : 'confirmed';
} elseif ($month == 11) {
$status = $i < 5 ? (rand(0, 1) ? 'invoiced' : 'confirmed') : 'draft';
} else {
$status = $i < 1 ? 'confirmed' : 'draft';
}
$tax = round($supply * 0.1);
$total = $supply + $tax;
Sale::create([
'tenant_id' => $tenantId,
'sale_number' => sprintf('SAL-%04d%02d-%04d', $year, $month, $i + 1),
'sale_date' => sprintf('%04d-%02d-%02d', $year, $month, $day),
'client_id' => $client->id,
'supply_amount' => $supply,
'tax_amount' => $tax,
'total_amount' => $total,
'description' => $clientName . ' 매출',
'status' => $status,
'created_by' => $userId,
]);
$count++;
}
}
$this->command->info(' ✓ sales: ' . $count . '건 생성');
}
}

View File

@@ -0,0 +1,87 @@
<?php
namespace Database\Seeders\Dummy;
use App\Models\Orders\Client;
use App\Models\Tenants\BankAccount;
use App\Models\Tenants\Withdrawal;
use Database\Seeders\DummyDataSeeder;
use Illuminate\Database\Seeder;
class DummyWithdrawalSeeder extends Seeder
{
public function run(): void
{
$tenantId = DummyDataSeeder::TENANT_ID;
$userId = DummyDataSeeder::USER_ID;
// 거래처 매핑 (PURCHASE, BOTH만)
$clients = Client::where('tenant_id', $tenantId)
->whereIn('client_type', ['PURCHASE', 'BOTH'])
->get()
->keyBy('name');
// 은행계좌
$primaryBankId = BankAccount::where('tenant_id', $tenantId)
->where('is_primary', true)
->value('id');
// 결제수단 분포
$methods = array_merge(
array_fill(0, 14, 'transfer'),
array_fill(0, 3, 'card'),
array_fill(0, 2, 'cash'),
array_fill(0, 1, 'check')
);
// 매입처 순환 목록
$clientNames = [
'한화솔루션', '포스코', '롯데케미칼', 'GS칼텍스', '대한항공',
'현대제철', 'SK이노베이션', 'CJ대한통운', '두산에너빌리티',
];
$amounts = [
'small' => [1000000, 5000000],
'medium' => [5000000, 30000000],
'large' => [30000000, 80000000],
];
$count = 0;
$year = 2025;
for ($month = 1; $month <= 12; $month++) {
$daysInMonth = cal_days_in_month(CAL_GREGORIAN, $month, $year);
for ($i = 0; $i < 5; $i++) {
$day = rand(1, $daysInMonth);
$clientName = $clientNames[($month * 5 + $i) % count($clientNames)];
$client = $clients->get($clientName);
$rand = rand(1, 100);
if ($rand <= 30) {
$amount = rand($amounts['small'][0], $amounts['small'][1]);
} elseif ($rand <= 80) {
$amount = rand($amounts['medium'][0], $amounts['medium'][1]);
} else {
$amount = rand($amounts['large'][0], $amounts['large'][1]);
}
Withdrawal::create([
'tenant_id' => $tenantId,
'withdrawal_date' => sprintf('%04d-%02d-%02d', $year, $month, $day),
'client_id' => $client?->id,
'client_name' => $client ? null : $clientName,
'bank_account_id' => $primaryBankId,
'amount' => $amount,
'payment_method' => $methods[array_rand($methods)],
'description' => $clientName . ' 지급',
'created_by' => $userId,
]);
$count++;
}
}
$this->command->info(' ✓ withdrawals: ' . $count . '건 생성');
}
}

View File

@@ -0,0 +1,54 @@
<?php
// database/seeders/DummyDataSeeder.php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
class DummyDataSeeder extends Seeder
{
// 대상 테넌트 ID
public const TENANT_ID = 287;
// 생성자 사용자 ID
public const USER_ID = 1;
public function run(): void
{
$this->command->info('🌱 더미 데이터 시딩 시작...');
$this->command->info(' 대상 테넌트: ID '.self::TENANT_ID);
$this->call([
Dummy\DummyClientGroupSeeder::class,
Dummy\DummyBankAccountSeeder::class,
Dummy\DummyClientSeeder::class,
Dummy\DummyDepositSeeder::class,
Dummy\DummyWithdrawalSeeder::class,
Dummy\DummySaleSeeder::class,
Dummy\DummyPurchaseSeeder::class,
Dummy\DummyPopupSeeder::class,
Dummy\DummyPaymentSeeder::class,
]);
$this->command->info('');
$this->command->info('✅ 더미 데이터 시딩 완료!');
$this->command->table(
['테이블', '생성 수량'],
[
['client_groups', '5'],
['bank_accounts', '5'],
['clients', '20'],
['deposits', '60'],
['withdrawals', '60'],
['sales', '80'],
['purchases', '70'],
['popups', '8'],
['plans', '6'],
['subscriptions', '1'],
['payments', '13'],
['총계', '~328'],
]
);
}
}

View File

@@ -1,172 +0,0 @@
{
"name": "ItemField is_active 컬럼 검증 테스트",
"description": "item_fields 테이블에 추가된 is_active 컬럼의 기능을 검증합니다. 필드 생성 시 기본값(true), 수정, 조회 시 is_active 필드 포함 여부를 테스트합니다.",
"version": "1.0",
"config": {
"baseUrl": "https://api.sam.kr/api/v1",
"apiKey": "{{$env.FLOW_TESTER_API_KEY}}",
"timeout": 30000,
"stopOnFailure": true
},
"variables": {
"user_id": "{{$env.FLOW_TESTER_USER_ID}}",
"user_pwd": "{{$env.FLOW_TESTER_USER_PWD}}"
},
"steps": [
{
"id": "login",
"name": "1. 로그인 - 인증 토큰 획득",
"method": "POST",
"endpoint": "/login",
"body": {
"user_id": "{{user_id}}",
"user_pwd": "{{user_pwd}}"
},
"expect": {
"status": [200],
"jsonPath": {
"$.message": "로그인 성공",
"$.access_token": "@isString"
}
},
"extract": {
"token": "$.access_token"
}
},
{
"id": "get_fields_list",
"name": "2. 필드 목록 조회 - is_active 필드 포함 확인",
"method": "GET",
"endpoint": "/item-master/fields",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true,
"$.data": "@isArray"
}
},
"extract": {
"existingFieldId": "$.data[0].id",
"fieldCount": "$.data.length"
}
},
{
"id": "create_field",
"name": "3. 독립 필드 생성 - is_active 기본값 true 확인",
"method": "POST",
"endpoint": "/item-master/fields",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"body": {
"field_name": "[테스트] is_active 검증 필드",
"field_type": "textbox",
"field_key": "test_is_active",
"is_required": false,
"placeholder": "is_active 기본값 테스트",
"description": "API Flow Tester에서 생성한 테스트 필드"
},
"expect": {
"status": [200, 201],
"jsonPath": {
"$.success": true,
"$.data.id": "@isNumber",
"$.data.field_name": "[테스트] is_active 검증 필드",
"$.data.is_active": true
}
},
"extract": {
"newFieldId": "$.data.id"
}
},
{
"id": "verify_field_created",
"name": "4. 생성된 필드 상세 확인 - is_active=true",
"method": "GET",
"endpoint": "/item-master/fields",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true
}
},
"extract": {
"allFields": "$.data"
}
},
{
"id": "update_field_inactive",
"name": "5. 필드 비활성화 - is_active=false로 수정",
"method": "PUT",
"endpoint": "/item-master/fields/{{create_field.newFieldId}}",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"body": {
"is_active": false
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true,
"$.data.is_active": false
}
}
},
{
"id": "verify_field_inactive",
"name": "6. 비활성화 상태 확인",
"method": "GET",
"endpoint": "/item-master/fields",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true
}
}
},
{
"id": "update_field_active",
"name": "7. 필드 재활성화 - is_active=true로 수정",
"method": "PUT",
"endpoint": "/item-master/fields/{{create_field.newFieldId}}",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"body": {
"is_active": true
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true,
"$.data.is_active": true
}
}
},
{
"id": "delete_test_field",
"name": "8. 테스트 필드 삭제 (정리)",
"method": "DELETE",
"endpoint": "/item-master/fields/{{create_field.newFieldId}}",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true
}
}
}
]
}

View File

@@ -1,277 +0,0 @@
{
"name": "단가 관리 CRUD 테스트",
"description": "단가(Pricing) API의 생성, 조회, 수정, 확정, 삭제 전체 플로우 테스트",
"version": "1.0",
"config": {
"baseUrl": "",
"timeout": 30000,
"stopOnFailure": true
},
"variables": {
"user_id": "{{$env.FLOW_TESTER_USER_ID}}",
"user_pwd": "{{$env.FLOW_TESTER_USER_PWD}}"
},
"steps": [
{
"id": "login",
"name": "로그인",
"method": "POST",
"endpoint": "/login",
"body": {
"user_id": "{{user_id}}",
"user_pwd": "{{user_pwd}}"
},
"expect": {
"status": [200],
"jsonPath": {
"$.message": "로그인 성공",
"$.access_token": "@isString"
}
},
"extract": {
"token": "$.access_token"
}
},
{
"id": "list_prices",
"name": "단가 목록 조회",
"method": "GET",
"endpoint": "/pricing",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"query": {
"per_page": 10,
"page": 1
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true,
"$.data.data": "@isArray"
}
}
},
{
"id": "create_price",
"name": "단가 생성 (MATERIAL)",
"method": "POST",
"endpoint": "/pricing",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"body": {
"item_type_code": "MATERIAL",
"item_id": 1,
"client_group_id": null,
"purchase_price": 10000,
"processing_cost": 500,
"loss_rate": 5,
"margin_rate": 20,
"sales_price": 12600,
"rounding_rule": "round",
"rounding_unit": 100,
"supplier": "테스트 공급업체",
"effective_from": "2025-01-01",
"effective_to": "2025-12-31",
"note": "API Flow 테스트용 단가",
"status": "draft"
},
"expect": {
"status": [201],
"jsonPath": {
"$.success": true,
"$.data.id": "@isNumber",
"$.data.item_type_code": "MATERIAL",
"$.data.purchase_price": 10000,
"$.data.status": "draft"
}
},
"extract": {
"price_id": "$.data.id"
}
},
{
"id": "show_price",
"name": "생성된 단가 상세 조회",
"method": "GET",
"endpoint": "/pricing/{{create_price.price_id}}",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true,
"$.data.id": "{{create_price.price_id}}",
"$.data.item_type_code": "MATERIAL",
"$.data.supplier": "테스트 공급업체"
}
}
},
{
"id": "update_price",
"name": "단가 수정 (가격 변경)",
"method": "PUT",
"endpoint": "/pricing/{{create_price.price_id}}",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"body": {
"purchase_price": 11000,
"processing_cost": 600,
"margin_rate": 25,
"sales_price": 14500,
"note": "단가 수정 테스트",
"change_reason": "원가 인상으로 인한 가격 조정",
"status": "active"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true,
"$.data.purchase_price": 11000,
"$.data.processing_cost": 600,
"$.data.status": "active"
}
}
},
{
"id": "get_revisions",
"name": "변경 이력 조회",
"method": "GET",
"endpoint": "/pricing/{{create_price.price_id}}/revisions",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true,
"$.data": "@isArray"
}
}
},
{
"id": "get_cost",
"name": "원가 조회 (receipt > standard 폴백)",
"method": "GET",
"endpoint": "/pricing/cost",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"query": {
"item_type_code": "MATERIAL",
"item_id": 1,
"date": "2025-06-15"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true,
"$.data.item_type_code": "MATERIAL",
"$.data.item_id": 1
}
}
},
{
"id": "by_items",
"name": "다중 품목 단가 조회",
"method": "POST",
"endpoint": "/pricing/by-items",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"body": {
"items": [
{
"item_type_code": "MATERIAL",
"item_id": 1
}
],
"date": "2025-06-15"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true,
"$.data": "@isArray"
}
}
},
{
"id": "create_price_for_finalize",
"name": "확정 테스트용 단가 생성",
"method": "POST",
"endpoint": "/pricing",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"body": {
"item_type_code": "PRODUCT",
"item_id": 1,
"purchase_price": 50000,
"sales_price": 70000,
"effective_from": "2025-01-01",
"status": "active"
},
"expect": {
"status": [201],
"jsonPath": {
"$.success": true,
"$.data.id": "@isNumber"
}
},
"extract": {
"finalize_price_id": "$.data.id"
}
},
{
"id": "finalize_price",
"name": "가격 확정 (불변 처리)",
"method": "POST",
"endpoint": "/pricing/{{create_price_for_finalize.finalize_price_id}}/finalize",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true,
"$.data.status": "finalized",
"$.data.is_final": true
}
}
},
{
"id": "delete_price",
"name": "단가 삭제 (soft delete)",
"method": "DELETE",
"endpoint": "/pricing/{{create_price.price_id}}",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true
}
}
},
{
"id": "verify_deleted",
"name": "삭제된 단가 조회 시 404 확인",
"method": "GET",
"endpoint": "/pricing/{{create_price.price_id}}",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"expect": {
"status": [404],
"jsonPath": {
"$.success": false
}
}
}
]
}

View File

@@ -1,227 +0,0 @@
{
"name": "Attendance API 근태관리 테스트",
"description": "근태 CRUD, 출퇴근 기록, 월간 통계 테스트",
"version": "1.0",
"config": {
"baseUrl": "",
"timeout": 30000,
"stopOnFailure": true
},
"variables": {
"user_id": "{{$env.FLOW_TESTER_USER_ID}}",
"user_pwd": "{{$env.FLOW_TESTER_USER_PWD}}",
"test_date": "{{$date}}"
},
"steps": [
{
"id": "login",
"name": "로그인",
"method": "POST",
"endpoint": "/login",
"body": {
"user_id": "{{user_id}}",
"user_pwd": "{{user_pwd}}"
},
"expect": {
"status": [200],
"jsonPath": {
"$.message": "로그인 성공",
"$.access_token": "@isString"
}
},
"extract": {
"token": "$.access_token",
"current_user_id": "$.user.id"
}
},
{
"id": "check_in",
"name": "출근 기록",
"method": "POST",
"endpoint": "/attendances/check-in",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"body": {
"check_in": "09:00:00",
"gps_data": {
"latitude": 37.5665,
"longitude": 126.978,
"accuracy": 10
}
},
"expect": {
"status": [200, 201],
"jsonPath": {
"$.success": true,
"$.data.id": "@isNumber",
"$.data.status": "@isString"
}
},
"extract": {
"attendance_id": "$.data.id"
}
},
{
"id": "show_attendance",
"name": "근태 상세 조회",
"method": "GET",
"endpoint": "/attendances/{{check_in.attendance_id}}",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true,
"$.data.id": "{{check_in.attendance_id}}",
"$.data.base_date": "@isString"
}
}
},
{
"id": "check_out",
"name": "퇴근 기록",
"method": "POST",
"endpoint": "/attendances/check-out",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"body": {
"check_out": "18:00:00",
"gps_data": {
"latitude": 37.5665,
"longitude": 126.978,
"accuracy": 15
}
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true,
"$.data.id": "@isNumber"
}
}
},
{
"id": "list_attendances",
"name": "근태 목록 조회",
"method": "GET",
"endpoint": "/attendances",
"query": {
"page": 1,
"per_page": 10,
"date": "{{test_date}}"
},
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true,
"$.data.data": "@isArray"
}
}
},
{
"id": "monthly_stats",
"name": "월간 통계 조회",
"method": "GET",
"endpoint": "/attendances/monthly-stats",
"query": {
"year": 2025,
"month": 12
},
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true
}
}
},
{
"id": "create_attendance",
"name": "근태 수동 등록 (관리자)",
"method": "POST",
"endpoint": "/attendances",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"body": {
"user_id": "{{login.current_user_id}}",
"base_date": "2025-12-01",
"status": "onTime",
"json_details": {
"check_in": "09:00:00",
"check_out": "18:00:00",
"work_minutes": 480
},
"remarks": "Flow Tester 테스트 데이터"
},
"expect": {
"status": [200, 201],
"jsonPath": {
"$.success": true,
"$.data.id": "@isNumber"
}
},
"extract": {
"manual_attendance_id": "$.data.id"
}
},
{
"id": "update_attendance",
"name": "근태 수정",
"method": "PATCH",
"endpoint": "/attendances/{{create_attendance.manual_attendance_id}}",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"body": {
"status": "late",
"remarks": "수정된 테스트 데이터"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true,
"$.data.status": "late"
}
}
},
{
"id": "delete_manual_attendance",
"name": "수동 등록 근태 삭제",
"method": "DELETE",
"endpoint": "/attendances/{{create_attendance.manual_attendance_id}}",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true
}
}
},
{
"id": "delete_checkin_attendance",
"name": "출퇴근 기록 삭제 (정리)",
"method": "DELETE",
"endpoint": "/attendances/{{check_in.attendance_id}}",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true
}
}
}
]
}

View File

@@ -1,87 +0,0 @@
{
"name": "Department Tree API 테스트",
"description": "부서 트리 조회 테스트",
"version": "1.0",
"config": {
"baseUrl": "",
"timeout": 30000,
"stopOnFailure": true
},
"variables": {
"user_id": "{{$env.FLOW_TESTER_USER_ID}}",
"user_pwd": "{{$env.FLOW_TESTER_USER_PWD}}"
},
"steps": [
{
"id": "login",
"name": "로그인",
"method": "POST",
"endpoint": "/login",
"body": {
"user_id": "{{user_id}}",
"user_pwd": "{{user_pwd}}"
},
"expect": {
"status": [200],
"jsonPath": {
"$.message": "로그인 성공",
"$.access_token": "@isString"
}
},
"extract": {
"token": "$.access_token"
}
},
{
"id": "get_tree",
"name": "부서 트리 조회",
"method": "GET",
"endpoint": "/departments/tree",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true,
"$.data": "@isArray"
}
}
},
{
"id": "get_tree_with_users",
"name": "부서 트리 조회 (사용자 포함)",
"method": "GET",
"endpoint": "/departments/tree",
"query": {
"with_users": true
},
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true,
"$.data": "@isArray"
}
}
},
{
"id": "list_departments",
"name": "부서 목록 조회",
"method": "GET",
"endpoint": "/departments",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true,
"$.data": "@isArray"
}
}
}
]
}

View File

@@ -1,188 +0,0 @@
{
"name": "Employee API CRUD 테스트",
"description": "사원 관리 API 전체 CRUD 및 계정 생성 테스트",
"version": "1.0",
"config": {
"baseUrl": "",
"timeout": 30000,
"stopOnFailure": true
},
"variables": {
"user_id": "{{$env.FLOW_TESTER_USER_ID}}",
"user_pwd": "{{$env.FLOW_TESTER_USER_PWD}}",
"test_email": "test.employee.{{$timestamp}}@example.com",
"test_name": "테스트사원{{$random:4}}"
},
"steps": [
{
"id": "login",
"name": "로그인",
"method": "POST",
"endpoint": "/login",
"body": {
"user_id": "{{user_id}}",
"user_pwd": "{{user_pwd}}"
},
"expect": {
"status": [200],
"jsonPath": {
"$.message": "로그인 성공",
"$.access_token": "@isString"
}
},
"extract": {
"token": "$.access_token",
"current_user_id": "$.user.id"
}
},
{
"id": "get_stats",
"name": "사원 통계 조회",
"method": "GET",
"endpoint": "/employees/stats",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true,
"$.data.total": "@isNumber",
"$.data.active": "@isNumber",
"$.data.leave": "@isNumber",
"$.data.resigned": "@isNumber"
}
}
},
{
"id": "list_employees",
"name": "사원 목록 조회",
"method": "GET",
"endpoint": "/employees",
"query": {
"page": 1,
"per_page": 10
},
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true,
"$.data.data": "@isArray"
}
}
},
{
"id": "create_employee",
"name": "사원 등록",
"method": "POST",
"endpoint": "/employees",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"body": {
"name": "{{test_name}}",
"email": "{{test_email}}",
"phone": "010-1234-5678",
"employee_number": "EMP{{$random:6}}",
"employee_status": "active",
"position": "사원",
"hire_date": "{{$date}}",
"json_extra": {
"emergency_contact": "010-9999-8888",
"address": "서울시 강남구"
}
},
"expect": {
"status": [200, 201],
"jsonPath": {
"$.success": true,
"$.data.id": "@isNumber",
"$.data.user.name": "{{test_name}}"
}
},
"extract": {
"employee_id": "$.data.id",
"user_id": "$.data.user_id"
}
},
{
"id": "show_employee",
"name": "사원 상세 조회",
"method": "GET",
"endpoint": "/employees/{{create_employee.employee_id}}",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true,
"$.data.id": "{{create_employee.employee_id}}",
"$.data.employee_status": "active"
}
}
},
{
"id": "update_employee",
"name": "사원 정보 수정",
"method": "PATCH",
"endpoint": "/employees/{{create_employee.employee_id}}",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"body": {
"position": "대리",
"employee_status": "active",
"json_extra": {
"emergency_contact": "010-1111-2222",
"address": "서울시 서초구",
"skills": ["Laravel", "React"]
}
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true,
"$.data.id": "@isNumber"
}
}
},
{
"id": "list_filtered",
"name": "사원 필터 조회 (재직자)",
"method": "GET",
"endpoint": "/employees",
"query": {
"status": "active",
"q": "{{test_name}}"
},
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true
}
}
},
{
"id": "delete_employee",
"name": "사원 삭제",
"method": "DELETE",
"endpoint": "/employees/{{create_employee.employee_id}}",
"headers": {
"Authorization": "Bearer {{login.token}}"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true
}
}
}
]
}

View File

@@ -1,926 +0,0 @@
{
"name": "사용자 초대 플로우 테스트",
"description": "사용자 초대 전체 플로우 (발송/목록/수락/취소/재발송) 및 분기 상황 테스트",
"version": "1.0",
"config": {
"baseUrl": "https://api.sam.kr/api/v1",
"apiKey": "{{$env.FLOW_TESTER_API_KEY}}",
"timeout": 30000,
"stopOnFailure": false
},
"variables": {
"user_id": "{{$env.FLOW_TESTER_USER_ID}}",
"user_pwd": "{{$env.FLOW_TESTER_USER_PWD}}",
"test_email_prefix": "flowtest",
"test_domain": "example.com"
},
"setup": {
"description": "테스트 전 초기화 - 기존 테스트 초대 정리는 수동으로 진행"
},
"flows": [
{
"id": "flow_1",
"name": "🔴 P1: 기본 초대 플로우 (role=user)",
"description": "일반 사용자 역할로 초대 발송 → 목록 확인 → 취소",
"steps": [
{
"id": "login",
"name": "로그인",
"method": "POST",
"endpoint": "/login",
"body": {
"user_id": "{{variables.user_id}}",
"user_pwd": "{{variables.user_pwd}}"
},
"extract": {
"accessToken": "$.access_token"
},
"expect": {
"status": [200],
"jsonPath": {
"$.access_token": "@isString"
}
}
},
{
"id": "invite_user_role",
"name": "초대 발송 (role=user)",
"method": "POST",
"endpoint": "/users/invite",
"headers": {
"Authorization": "Bearer {{login.accessToken}}"
},
"body": {
"email": "{{variables.test_email_prefix}}_user_{{$timestamp}}@{{variables.test_domain}}",
"role": "user",
"message": "SAM 시스템에 합류해 주세요!"
},
"dependsOn": ["login"],
"extract": {
"invitationId": "$.data.id",
"invitationToken": "$.data.token",
"invitedEmail": "$.data.email"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true,
"$.data.status": "pending",
"$.data.email": "@isString"
}
}
},
{
"id": "list_invitations",
"name": "초대 목록 조회",
"method": "GET",
"endpoint": "/users/invitations",
"headers": {
"Authorization": "Bearer {{login.accessToken}}"
},
"query": {
"status": "pending",
"per_page": 10
},
"dependsOn": ["invite_user_role"],
"expect": {
"status": [200],
"jsonPath": {
"$.success": true,
"$.data.data": "@isArray"
}
}
},
{
"id": "cancel_invitation",
"name": "초대 취소",
"method": "DELETE",
"endpoint": "/users/invitations/{{invite_user_role.invitationId}}",
"headers": {
"Authorization": "Bearer {{login.accessToken}}"
},
"dependsOn": ["list_invitations"],
"expect": {
"status": [200],
"jsonPath": {
"$.success": true
}
}
}
]
},
{
"id": "flow_2",
"name": "🔴 P1: 관리자 역할 초대 (role=admin)",
"description": "관리자 역할로 초대 발송",
"steps": [
{
"id": "login",
"name": "로그인",
"method": "POST",
"endpoint": "/login",
"body": {
"user_id": "{{variables.user_id}}",
"user_pwd": "{{variables.user_pwd}}"
},
"extract": {
"accessToken": "$.access_token"
},
"expect": {
"status": [200]
}
},
{
"id": "invite_admin_role",
"name": "초대 발송 (role=admin)",
"method": "POST",
"endpoint": "/users/invite",
"headers": {
"Authorization": "Bearer {{login.accessToken}}"
},
"body": {
"email": "{{variables.test_email_prefix}}_admin_{{$timestamp}}@{{variables.test_domain}}",
"role": "admin",
"message": "관리자로 초대합니다."
},
"dependsOn": ["login"],
"extract": {
"invitationId": "$.data.id"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true,
"$.data.status": "pending"
}
}
},
{
"id": "cleanup",
"name": "초대 취소 (정리)",
"method": "DELETE",
"endpoint": "/users/invitations/{{invite_admin_role.invitationId}}",
"headers": {
"Authorization": "Bearer {{login.accessToken}}"
},
"dependsOn": ["invite_admin_role"],
"expect": {
"status": [200]
}
}
]
},
{
"id": "flow_3",
"name": "🔴 P1: 매니저 역할 초대 (role=manager)",
"description": "매니저 역할로 초대 발송",
"steps": [
{
"id": "login",
"name": "로그인",
"method": "POST",
"endpoint": "/login",
"body": {
"user_id": "{{variables.user_id}}",
"user_pwd": "{{variables.user_pwd}}"
},
"extract": {
"accessToken": "$.access_token"
},
"expect": {
"status": [200]
}
},
{
"id": "invite_manager_role",
"name": "초대 발송 (role=manager)",
"method": "POST",
"endpoint": "/users/invite",
"headers": {
"Authorization": "Bearer {{login.accessToken}}"
},
"body": {
"email": "{{variables.test_email_prefix}}_manager_{{$timestamp}}@{{variables.test_domain}}",
"role": "manager"
},
"dependsOn": ["login"],
"extract": {
"invitationId": "$.data.id"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true,
"$.data.status": "pending"
}
}
},
{
"id": "cleanup",
"name": "초대 취소 (정리)",
"method": "DELETE",
"endpoint": "/users/invitations/{{invite_manager_role.invitationId}}",
"headers": {
"Authorization": "Bearer {{login.accessToken}}"
},
"dependsOn": ["invite_manager_role"],
"expect": {
"status": [200]
}
}
]
},
{
"id": "flow_4",
"name": "🔴 P1: 초대 재발송 플로우",
"description": "초대 발송 → 재발송 → 취소",
"steps": [
{
"id": "login",
"name": "로그인",
"method": "POST",
"endpoint": "/login",
"body": {
"user_id": "{{variables.user_id}}",
"user_pwd": "{{variables.user_pwd}}"
},
"extract": {
"accessToken": "$.access_token"
},
"expect": {
"status": [200]
}
},
{
"id": "invite",
"name": "초대 발송",
"method": "POST",
"endpoint": "/users/invite",
"headers": {
"Authorization": "Bearer {{login.accessToken}}"
},
"body": {
"email": "{{variables.test_email_prefix}}_resend_{{$timestamp}}@{{variables.test_domain}}",
"role": "user"
},
"dependsOn": ["login"],
"extract": {
"invitationId": "$.data.id",
"originalExpiresAt": "$.data.expires_at"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true
}
}
},
{
"id": "resend",
"name": "초대 재발송",
"method": "POST",
"endpoint": "/users/invitations/{{invite.invitationId}}/resend",
"headers": {
"Authorization": "Bearer {{login.accessToken}}"
},
"dependsOn": ["invite"],
"extract": {
"newExpiresAt": "$.data.expires_at"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true,
"$.data.status": "pending"
}
}
},
{
"id": "cleanup",
"name": "초대 취소 (정리)",
"method": "DELETE",
"endpoint": "/users/invitations/{{invite.invitationId}}",
"headers": {
"Authorization": "Bearer {{login.accessToken}}"
},
"dependsOn": ["resend"],
"expect": {
"status": [200]
}
}
]
},
{
"id": "flow_5",
"name": "🔴 P1: 에러 케이스 - 중복 이메일 초대",
"description": "동일 이메일로 중복 초대 시도 (대기 중 초대 존재)",
"steps": [
{
"id": "login",
"name": "로그인",
"method": "POST",
"endpoint": "/login",
"body": {
"user_id": "{{variables.user_id}}",
"user_pwd": "{{variables.user_pwd}}"
},
"extract": {
"accessToken": "$.access_token"
},
"expect": {
"status": [200]
}
},
{
"id": "first_invite",
"name": "첫 번째 초대 발송",
"method": "POST",
"endpoint": "/users/invite",
"headers": {
"Authorization": "Bearer {{login.accessToken}}"
},
"body": {
"email": "{{variables.test_email_prefix}}_dup_{{$timestamp}}@{{variables.test_domain}}",
"role": "user"
},
"dependsOn": ["login"],
"extract": {
"invitationId": "$.data.id",
"testEmail": "$.data.email"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true
}
}
},
{
"id": "duplicate_invite",
"name": "중복 초대 발송 (실패 예상)",
"method": "POST",
"endpoint": "/users/invite",
"headers": {
"Authorization": "Bearer {{login.accessToken}}"
},
"body": {
"email": "{{first_invite.testEmail}}",
"role": "user"
},
"dependsOn": ["first_invite"],
"expect": {
"status": [400, 422],
"jsonPath": {
"$.success": false
}
}
},
{
"id": "cleanup",
"name": "초대 취소 (정리)",
"method": "DELETE",
"endpoint": "/users/invitations/{{first_invite.invitationId}}",
"headers": {
"Authorization": "Bearer {{login.accessToken}}"
},
"dependsOn": ["duplicate_invite"],
"expect": {
"status": [200]
}
}
]
},
{
"id": "flow_6",
"name": "🟡 P2: 목록 필터링 - 상태별",
"description": "초대 목록을 상태별로 필터링",
"steps": [
{
"id": "login",
"name": "로그인",
"method": "POST",
"endpoint": "/login",
"body": {
"user_id": "{{variables.user_id}}",
"user_pwd": "{{variables.user_pwd}}"
},
"extract": {
"accessToken": "$.access_token"
},
"expect": {
"status": [200]
}
},
{
"id": "list_pending",
"name": "대기 중 목록 조회",
"method": "GET",
"endpoint": "/users/invitations",
"headers": {
"Authorization": "Bearer {{login.accessToken}}"
},
"query": {
"status": "pending"
},
"dependsOn": ["login"],
"expect": {
"status": [200],
"jsonPath": {
"$.success": true
}
}
},
{
"id": "list_accepted",
"name": "수락된 목록 조회",
"method": "GET",
"endpoint": "/users/invitations",
"headers": {
"Authorization": "Bearer {{login.accessToken}}"
},
"query": {
"status": "accepted"
},
"dependsOn": ["login"],
"expect": {
"status": [200],
"jsonPath": {
"$.success": true
}
}
},
{
"id": "list_expired",
"name": "만료된 목록 조회",
"method": "GET",
"endpoint": "/users/invitations",
"headers": {
"Authorization": "Bearer {{login.accessToken}}"
},
"query": {
"status": "expired"
},
"dependsOn": ["login"],
"expect": {
"status": [200],
"jsonPath": {
"$.success": true
}
}
},
{
"id": "list_cancelled",
"name": "취소된 목록 조회",
"method": "GET",
"endpoint": "/users/invitations",
"headers": {
"Authorization": "Bearer {{login.accessToken}}"
},
"query": {
"status": "cancelled"
},
"dependsOn": ["login"],
"expect": {
"status": [200],
"jsonPath": {
"$.success": true
}
}
}
]
},
{
"id": "flow_7",
"name": "🟡 P2: 목록 정렬 옵션",
"description": "다양한 정렬 기준으로 목록 조회",
"steps": [
{
"id": "login",
"name": "로그인",
"method": "POST",
"endpoint": "/login",
"body": {
"user_id": "{{variables.user_id}}",
"user_pwd": "{{variables.user_pwd}}"
},
"extract": {
"accessToken": "$.access_token"
},
"expect": {
"status": [200]
}
},
{
"id": "sort_created_at_desc",
"name": "생성일 내림차순",
"method": "GET",
"endpoint": "/users/invitations",
"headers": {
"Authorization": "Bearer {{login.accessToken}}"
},
"query": {
"sort_by": "created_at",
"sort_dir": "desc"
},
"dependsOn": ["login"],
"expect": {
"status": [200],
"jsonPath": {
"$.success": true
}
}
},
{
"id": "sort_expires_at_asc",
"name": "만료일 오름차순",
"method": "GET",
"endpoint": "/users/invitations",
"headers": {
"Authorization": "Bearer {{login.accessToken}}"
},
"query": {
"sort_by": "expires_at",
"sort_dir": "asc"
},
"dependsOn": ["login"],
"expect": {
"status": [200],
"jsonPath": {
"$.success": true
}
}
},
{
"id": "sort_email_asc",
"name": "이메일 오름차순",
"method": "GET",
"endpoint": "/users/invitations",
"headers": {
"Authorization": "Bearer {{login.accessToken}}"
},
"query": {
"sort_by": "email",
"sort_dir": "asc"
},
"dependsOn": ["login"],
"expect": {
"status": [200],
"jsonPath": {
"$.success": true
}
}
}
]
},
{
"id": "flow_8",
"name": "🟡 P2: 이메일 검색",
"description": "이메일 키워드로 초대 검색",
"steps": [
{
"id": "login",
"name": "로그인",
"method": "POST",
"endpoint": "/login",
"body": {
"user_id": "{{variables.user_id}}",
"user_pwd": "{{variables.user_pwd}}"
},
"extract": {
"accessToken": "$.access_token"
},
"expect": {
"status": [200]
}
},
{
"id": "search_by_email",
"name": "이메일 검색",
"method": "GET",
"endpoint": "/users/invitations",
"headers": {
"Authorization": "Bearer {{login.accessToken}}"
},
"query": {
"search": "flowtest"
},
"dependsOn": ["login"],
"expect": {
"status": [200],
"jsonPath": {
"$.success": true,
"$.data.data": "@isArray"
}
}
}
]
},
{
"id": "flow_9",
"name": "🟢 P3: 만료 기간 지정 옵션",
"description": "만료 기간을 명시적으로 지정하여 초대",
"steps": [
{
"id": "login",
"name": "로그인",
"method": "POST",
"endpoint": "/login",
"body": {
"user_id": "{{variables.user_id}}",
"user_pwd": "{{variables.user_pwd}}"
},
"extract": {
"accessToken": "$.access_token"
},
"expect": {
"status": [200]
}
},
{
"id": "invite_with_expires",
"name": "14일 만료 기간으로 초대",
"method": "POST",
"endpoint": "/users/invite",
"headers": {
"Authorization": "Bearer {{login.accessToken}}"
},
"body": {
"email": "{{variables.test_email_prefix}}_expires_{{$timestamp}}@{{variables.test_domain}}",
"role": "user",
"expires_days": 14
},
"dependsOn": ["login"],
"extract": {
"invitationId": "$.data.id"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true,
"$.data.expires_at": "@isString"
}
}
},
{
"id": "cleanup",
"name": "초대 취소 (정리)",
"method": "DELETE",
"endpoint": "/users/invitations/{{invite_with_expires.invitationId}}",
"headers": {
"Authorization": "Bearer {{login.accessToken}}"
},
"dependsOn": ["invite_with_expires"],
"expect": {
"status": [200]
}
}
]
},
{
"id": "flow_10",
"name": "🟢 P3: role_id로 초대 (숫자 ID 사용)",
"description": "role 문자열 대신 role_id 숫자로 초대",
"steps": [
{
"id": "login",
"name": "로그인",
"method": "POST",
"endpoint": "/login",
"body": {
"user_id": "{{variables.user_id}}",
"user_pwd": "{{variables.user_pwd}}"
},
"extract": {
"accessToken": "$.access_token"
},
"expect": {
"status": [200]
}
},
{
"id": "invite_with_role_id",
"name": "role_id=2로 초대",
"method": "POST",
"endpoint": "/users/invite",
"headers": {
"Authorization": "Bearer {{login.accessToken}}"
},
"body": {
"email": "{{variables.test_email_prefix}}_roleid_{{$timestamp}}@{{variables.test_domain}}",
"role_id": 2
},
"dependsOn": ["login"],
"extract": {
"invitationId": "$.data.id"
},
"expect": {
"status": [200],
"jsonPath": {
"$.success": true,
"$.data.role_id": 2
}
}
},
{
"id": "cleanup",
"name": "초대 취소 (정리)",
"method": "DELETE",
"endpoint": "/users/invitations/{{invite_with_role_id.invitationId}}",
"headers": {
"Authorization": "Bearer {{login.accessToken}}"
},
"dependsOn": ["invite_with_role_id"],
"expect": {
"status": [200]
}
}
]
},
{
"id": "flow_11",
"name": "🔴 P1: 에러 케이스 - 잘못된 역할",
"description": "존재하지 않는 role 값으로 초대 시도",
"steps": [
{
"id": "login",
"name": "로그인",
"method": "POST",
"endpoint": "/login",
"body": {
"user_id": "{{variables.user_id}}",
"user_pwd": "{{variables.user_pwd}}"
},
"extract": {
"accessToken": "$.access_token"
},
"expect": {
"status": [200]
}
},
{
"id": "invite_invalid_role",
"name": "잘못된 role로 초대 (실패 예상)",
"method": "POST",
"endpoint": "/users/invite",
"headers": {
"Authorization": "Bearer {{login.accessToken}}"
},
"body": {
"email": "{{variables.test_email_prefix}}_invalid_{{$timestamp}}@{{variables.test_domain}}",
"role": "superadmin"
},
"dependsOn": ["login"],
"expect": {
"status": [400, 422],
"jsonPath": {
"$.success": false
}
}
}
]
},
{
"id": "flow_12",
"name": "🔴 P1: 에러 케이스 - 이메일 형식 오류",
"description": "잘못된 이메일 형식으로 초대 시도",
"steps": [
{
"id": "login",
"name": "로그인",
"method": "POST",
"endpoint": "/login",
"body": {
"user_id": "{{variables.user_id}}",
"user_pwd": "{{variables.user_pwd}}"
},
"extract": {
"accessToken": "$.access_token"
},
"expect": {
"status": [200]
}
},
{
"id": "invite_invalid_email",
"name": "잘못된 이메일로 초대 (실패 예상)",
"method": "POST",
"endpoint": "/users/invite",
"headers": {
"Authorization": "Bearer {{login.accessToken}}"
},
"body": {
"email": "invalid-email-format",
"role": "user"
},
"dependsOn": ["login"],
"expect": {
"status": [422],
"jsonPath": {
"$.success": false
}
}
}
]
},
{
"id": "flow_13",
"name": "🔴 P1: 에러 케이스 - 권한 없이 접근",
"description": "인증 없이 초대 API 접근 시도",
"steps": [
{
"id": "invite_no_auth",
"name": "인증 없이 초대 시도 (실패 예상)",
"method": "POST",
"endpoint": "/users/invite",
"body": {
"email": "noauth@example.com",
"role": "user"
},
"expect": {
"status": [401],
"jsonPath": {
"$.success": false
}
}
},
{
"id": "list_no_auth",
"name": "인증 없이 목록 조회 (실패 예상)",
"method": "GET",
"endpoint": "/users/invitations",
"expect": {
"status": [401],
"jsonPath": {
"$.success": false
}
}
}
]
},
{
"id": "flow_14",
"name": "🔴 P1: 에러 케이스 - 존재하지 않는 초대 취소",
"description": "존재하지 않는 초대 ID로 취소 시도",
"steps": [
{
"id": "login",
"name": "로그인",
"method": "POST",
"endpoint": "/login",
"body": {
"user_id": "{{variables.user_id}}",
"user_pwd": "{{variables.user_pwd}}"
},
"extract": {
"accessToken": "$.access_token"
},
"expect": {
"status": [200]
}
},
{
"id": "cancel_not_found",
"name": "존재하지 않는 초대 취소 (실패 예상)",
"method": "DELETE",
"endpoint": "/users/invitations/999999",
"headers": {
"Authorization": "Bearer {{login.accessToken}}"
},
"dependsOn": ["login"],
"expect": {
"status": [404],
"jsonPath": {
"$.success": false
}
}
}
]
}
],
"summary": {
"total_flows": 14,
"priority_breakdown": {
"P1_critical": 8,
"P2_important": 3,
"P3_recommended": 3
},
"coverage": {
"endpoints": [
"POST /users/invite",
"GET /users/invitations",
"DELETE /users/invitations/{id}",
"POST /users/invitations/{id}/resend"
],
"not_covered": [
"POST /users/invitations/{token}/accept (별도 Flow 필요: 실제 이메일 수신 필요)"
]
},
"branches_tested": {
"role_types": ["admin", "manager", "user"],
"role_methods": ["role (string)", "role_id (integer)"],
"status_filters": ["pending", "accepted", "expired", "cancelled"],
"sort_options": ["created_at", "expires_at", "email"],
"error_cases": ["duplicate_email", "invalid_role", "invalid_email", "no_auth", "not_found"]
}
}
}

View File

@@ -375,4 +375,11 @@
'request_approved' => '회사 추가 신청이 승인되었습니다.', 'request_approved' => '회사 추가 신청이 승인되었습니다.',
'request_rejected' => '회사 추가 신청이 반려되었습니다.', 'request_rejected' => '회사 추가 신청이 반려되었습니다.',
], ],
// FCM 푸시 알림 관리
'fcm' => [
'sent' => 'FCM 발송이 완료되었습니다.',
'token_toggled' => '토큰 상태가 변경되었습니다.',
'token_deleted' => '토큰이 삭제되었습니다.',
],
]; ];