diff --git a/app/Http/Controllers/Api/V1/OrgChartController.php b/app/Http/Controllers/Api/V1/OrgChartController.php new file mode 100644 index 00000000..63ae2fd1 --- /dev/null +++ b/app/Http/Controllers/Api/V1/OrgChartController.php @@ -0,0 +1,82 @@ +service->getOrgChart($request->all()); + }, __('message.fetched')); + } + + // GET /v1/org-chart/stats + public function stats() + { + return ApiResponse::handle(function () { + return $this->service->getStats(); + }, __('message.fetched')); + } + + // GET /v1/org-chart/unassigned + public function unassigned(Request $request) + { + return ApiResponse::handle(function () use ($request) { + return $this->service->getUnassigned($request->all()); + }, __('message.fetched')); + } + + // POST /v1/org-chart/assign + public function assign(AssignRequest $request) + { + return ApiResponse::handle(function () use ($request) { + return $this->service->assign($request->validated()); + }, __('message.updated')); + } + + // POST /v1/org-chart/unassign + public function unassign(UnassignRequest $request) + { + return ApiResponse::handle(function () use ($request) { + return $this->service->unassign($request->validated()); + }, __('message.updated')); + } + + // PUT /v1/org-chart/reorder-employees + public function reorderEmployees(ReorderEmployeesRequest $request) + { + return ApiResponse::handle(function () use ($request) { + return $this->service->reorderEmployees($request->validated()); + }, __('message.reordered')); + } + + // PUT /v1/org-chart/reorder-departments + public function reorderDepartments(ReorderDepartmentsRequest $request) + { + return ApiResponse::handle(function () use ($request) { + return $this->service->reorderDepartments($request->validated()); + }, __('message.reordered')); + } + + // PATCH /v1/org-chart/departments/{id}/toggle-hide + public function toggleHide(int $id, ToggleHideRequest $request) + { + return ApiResponse::handle(function () use ($id, $request) { + return $this->service->toggleHide($id, $request->validated()); + }, __('message.updated')); + } +} diff --git a/app/Http/Requests/OrgChart/AssignRequest.php b/app/Http/Requests/OrgChart/AssignRequest.php new file mode 100644 index 00000000..40a6aba0 --- /dev/null +++ b/app/Http/Requests/OrgChart/AssignRequest.php @@ -0,0 +1,21 @@ + 'required|integer', + 'department_id' => 'required|integer', + ]; + } +} diff --git a/app/Http/Requests/OrgChart/ReorderDepartmentsRequest.php b/app/Http/Requests/OrgChart/ReorderDepartmentsRequest.php new file mode 100644 index 00000000..cf90e6d3 --- /dev/null +++ b/app/Http/Requests/OrgChart/ReorderDepartmentsRequest.php @@ -0,0 +1,23 @@ + 'required|array', + 'orders.*.id' => 'required|integer', + 'orders.*.parent_id' => 'nullable|integer', + 'orders.*.sort_order' => 'required|integer', + ]; + } +} diff --git a/app/Http/Requests/OrgChart/ReorderEmployeesRequest.php b/app/Http/Requests/OrgChart/ReorderEmployeesRequest.php new file mode 100644 index 00000000..36c5a313 --- /dev/null +++ b/app/Http/Requests/OrgChart/ReorderEmployeesRequest.php @@ -0,0 +1,22 @@ + 'required|array', + 'moves.*.employee_id' => 'required|integer', + 'moves.*.department_id' => 'nullable|integer', + ]; + } +} diff --git a/app/Http/Requests/OrgChart/ToggleHideRequest.php b/app/Http/Requests/OrgChart/ToggleHideRequest.php new file mode 100644 index 00000000..2c035b84 --- /dev/null +++ b/app/Http/Requests/OrgChart/ToggleHideRequest.php @@ -0,0 +1,20 @@ + 'required|boolean', + ]; + } +} diff --git a/app/Http/Requests/OrgChart/UnassignRequest.php b/app/Http/Requests/OrgChart/UnassignRequest.php new file mode 100644 index 00000000..78c5fba5 --- /dev/null +++ b/app/Http/Requests/OrgChart/UnassignRequest.php @@ -0,0 +1,20 @@ + 'required|integer', + ]; + } +} diff --git a/app/Models/Tenants/Department.php b/app/Models/Tenants/Department.php index 91eee016..071a87a0 100644 --- a/app/Models/Tenants/Department.php +++ b/app/Models/Tenants/Department.php @@ -28,6 +28,7 @@ class Department extends Model 'parent_id' => 'int', 'is_active' => 'bool', 'sort_order' => 'int', + 'options' => 'array', ]; protected $hidden = [ diff --git a/app/Services/OrgChartService.php b/app/Services/OrgChartService.php new file mode 100644 index 00000000..35f30c2c --- /dev/null +++ b/app/Services/OrgChartService.php @@ -0,0 +1,308 @@ +tenantId(); + + $includeHidden = filter_var($params['include_hidden'] ?? true, FILTER_VALIDATE_BOOLEAN); + + // 부서 전체 조회 (트리 구성을 위해 flat으로 가져옴) + $deptQuery = Department::where('tenant_id', $tenantId) + ->where('is_active', true); + + if (! $includeHidden) { + $deptQuery->where(function ($q) { + $q->whereNull('options->orgchart_hidden') + ->orWhere('options->orgchart_hidden', false); + }); + } + + $departments = $deptQuery + ->orderBy('sort_order') + ->orderBy('name') + ->get(); + + // 전체 활성 직원 + $employees = TenantUserProfile::where('tenant_id', $tenantId) + ->active() + ->with(['user:id,name,email']) + ->orderBy('display_name') + ->get() + ->map(fn ($e) => [ + 'id' => $e->id, + 'user_id' => $e->user_id, + 'department_id' => $e->department_id, + 'display_name' => $e->display_name ?? $e->user?->name ?? '(이름없음)', + 'position_label' => $e->position_label, + ]) + ->values(); + + // 회사 정보 + $tenant = \App\Models\Tenants\Tenant::find($tenantId); + + // 통계 + $total = $employees->count(); + $assigned = $employees->whereNotNull('department_id')->count(); + + // 부서 트리 구성 + $deptTree = $this->buildTree($departments, $employees); + + // 숨겨진 부서 목록 (include_hidden=true일 때만 의미) + $hiddenDepts = $departments->filter(fn ($d) => ($d->options['orgchart_hidden'] ?? false) === true) + ->map(fn ($d) => ['id' => $d->id, 'name' => $d->name, 'code' => $d->code]) + ->values(); + + return [ + 'company' => [ + 'name' => $tenant->company_name ?? 'SAM', + 'ceo_name' => $tenant->ceo_name ?? '', + ], + 'departments' => $deptTree, + 'hidden_departments' => $hiddenDepts, + 'unassigned' => $employees->whereNull('department_id')->values(), + 'stats' => [ + 'total' => $total, + 'assigned' => $assigned, + 'unassigned' => $total - $assigned, + ], + ]; + } + + /** + * 조직도 통계 + */ + public function getStats(): array + { + $tenantId = $this->tenantId(); + + $total = TenantUserProfile::where('tenant_id', $tenantId)->active()->count(); + $assigned = TenantUserProfile::where('tenant_id', $tenantId)->active()->whereNotNull('department_id')->count(); + + return [ + 'total' => $total, + 'assigned' => $assigned, + 'unassigned' => $total - $assigned, + ]; + } + + /** + * 미배치 직원 목록 + */ + public function getUnassigned(array $params): array + { + $tenantId = $this->tenantId(); + + $query = TenantUserProfile::where('tenant_id', $tenantId) + ->active() + ->whereNull('department_id') + ->with(['user:id,name,email']) + ->orderBy('display_name'); + + if (! empty($params['q'])) { + $q = $params['q']; + $query->where(function ($w) use ($q) { + $w->where('display_name', 'like', "%{$q}%") + ->orWhereHas('user', function ($u) use ($q) { + $u->where('name', 'like', "%{$q}%"); + }); + }); + } + + return $query->get() + ->map(fn ($e) => [ + 'id' => $e->id, + 'user_id' => $e->user_id, + 'display_name' => $e->display_name ?? $e->user?->name ?? '(이름없음)', + 'position_label' => $e->position_label, + ]) + ->values() + ->toArray(); + } + + /** + * 직원 부서 배치 + */ + public function assign(array $params): array + { + $tenantId = $this->tenantId(); + + $employee = TenantUserProfile::where('tenant_id', $tenantId) + ->where('id', $params['employee_id']) + ->first(); + + if (! $employee) { + return ['error' => __('error.not_found'), 'code' => 404]; + } + + $dept = Department::where('tenant_id', $tenantId) + ->where('id', $params['department_id']) + ->first(); + + if (! $dept) { + return ['error' => __('error.not_found'), 'code' => 404]; + } + + $employee->department_id = $params['department_id']; + $employee->save(); + + return [ + 'employee_id' => $employee->id, + 'department_id' => $params['department_id'], + ]; + } + + /** + * 직원 미배치 처리 + */ + public function unassign(array $params): array + { + $tenantId = $this->tenantId(); + + $employee = TenantUserProfile::where('tenant_id', $tenantId) + ->where('id', $params['employee_id']) + ->first(); + + if (! $employee) { + return ['error' => __('error.not_found'), 'code' => 404]; + } + + $employee->department_id = null; + $employee->save(); + + return ['employee_id' => $employee->id, 'department_id' => null]; + } + + /** + * 직원 일괄 배치/이동 + */ + public function reorderEmployees(array $params): array + { + $tenantId = $this->tenantId(); + + DB::transaction(function () use ($params, $tenantId) { + foreach ($params['moves'] as $move) { + TenantUserProfile::where('tenant_id', $tenantId) + ->where('id', $move['employee_id']) + ->update(['department_id' => $move['department_id']]); + } + }); + + return ['processed' => count($params['moves'])]; + } + + /** + * 부서 순서/계층 일괄 변경 + */ + public function reorderDepartments(array $params): array + { + $tenantId = $this->tenantId(); + + // 순환 참조 검증 + $orders = collect($params['orders']); + foreach ($orders as $order) { + if ($order['parent_id'] !== null && $order['parent_id'] === $order['id']) { + return ['error' => '자기 자신을 상위 부서로 설정할 수 없습니다.', 'code' => 422]; + } + } + + // 순환 참조 심층 검증 (A→B→C→A 같은 케이스) + $parentMap = $orders->pluck('parent_id', 'id')->toArray(); + foreach ($parentMap as $id => $parentId) { + if ($parentId === null) { + continue; + } + $visited = [$id]; + $current = $parentId; + while ($current !== null && isset($parentMap[$current])) { + if (in_array($current, $visited)) { + return ['error' => '순환 참조가 감지되었습니다.', 'code' => 422]; + } + $visited[] = $current; + $current = $parentMap[$current]; + } + } + + DB::transaction(function () use ($params, $tenantId) { + foreach ($params['orders'] as $order) { + Department::where('tenant_id', $tenantId) + ->where('id', $order['id']) + ->update([ + 'parent_id' => $order['parent_id'], + 'sort_order' => $order['sort_order'], + ]); + } + }); + + return ['processed' => count($params['orders'])]; + } + + /** + * 부서 숨기기/표시 토글 + */ + public function toggleHide(int $departmentId, array $params): array + { + $tenantId = $this->tenantId(); + + $dept = Department::where('tenant_id', $tenantId) + ->where('id', $departmentId) + ->first(); + + if (! $dept) { + return ['error' => __('error.not_found'), 'code' => 404]; + } + + $options = $dept->options ?? []; + $options['orgchart_hidden'] = $params['hidden']; + $dept->options = $options; + $dept->save(); + + return [ + 'id' => $dept->id, + 'orgchart_hidden' => $params['hidden'], + ]; + } + + /** + * flat 부서 목록 → 트리 구조 변환 + */ + private function buildTree($departments, $employees): array + { + $deptMap = []; + foreach ($departments as $dept) { + $deptMap[$dept->id] = [ + 'id' => $dept->id, + 'name' => $dept->name, + 'code' => $dept->code, + 'parent_id' => $dept->parent_id, + 'sort_order' => $dept->sort_order, + 'is_active' => $dept->is_active, + 'orgchart_hidden' => $dept->options['orgchart_hidden'] ?? false, + 'children' => [], + 'employees' => $employees->where('department_id', $dept->id)->values()->toArray(), + ]; + } + + $tree = []; + foreach ($deptMap as $id => &$node) { + if ($node['parent_id'] === null || ! isset($deptMap[$node['parent_id']])) { + $tree[] = &$node; + } else { + $deptMap[$node['parent_id']]['children'][] = &$node; + } + } + unset($node); + + return $tree; + } +} diff --git a/app/Swagger/v1/OrgChartApi.php b/app/Swagger/v1/OrgChartApi.php new file mode 100644 index 00000000..bbf454ae --- /dev/null +++ b/app/Swagger/v1/OrgChartApi.php @@ -0,0 +1,359 @@ +name('v1.departments.permissions.revoke'); // 권한 제거(해당 메뉴 범위까지) }); +// OrgChart API (조직도 관리) +Route::prefix('org-chart')->group(function () { + Route::get('', [OrgChartController::class, 'index'])->name('v1.org-chart.index'); + Route::get('/stats', [OrgChartController::class, 'stats'])->name('v1.org-chart.stats'); + Route::get('/unassigned', [OrgChartController::class, 'unassigned'])->name('v1.org-chart.unassigned'); + Route::post('/assign', [OrgChartController::class, 'assign'])->name('v1.org-chart.assign'); + Route::post('/unassign', [OrgChartController::class, 'unassign'])->name('v1.org-chart.unassign'); + Route::put('/reorder-employees', [OrgChartController::class, 'reorderEmployees'])->name('v1.org-chart.reorder-employees'); + Route::put('/reorder-departments', [OrgChartController::class, 'reorderDepartments'])->name('v1.org-chart.reorder-departments'); + Route::patch('/departments/{id}/toggle-hide', [OrgChartController::class, 'toggleHide'])->whereNumber('id')->name('v1.org-chart.toggle-hide'); +}); + // Position API (직급/직책 통합 관리) Route::prefix('positions')->group(function () { Route::get('', [PositionController::class, 'index'])->name('v1.positions.index'); diff --git a/storage/api-docs/api-docs-v1.json b/storage/api-docs/api-docs-v1.json index 4b4683ab..e0dc63ad 100644 --- a/storage/api-docs/api-docs-v1.json +++ b/storage/api-docs/api-docs-v1.json @@ -11,7 +11,7 @@ "servers": [ { "url": "https://api.sam.kr/", - "description": "SAM API 서버" + "description": "SAM관리시스템 API 서버" } ], "paths": { @@ -9577,9 +9577,15 @@ ], "properties": { "code": { - "description": "LOT 코드", + "description": "코드 체계 (prod+spec)", "type": "string", - "example": "RM260319" + "example": "RM" + }, + "lot_no": { + "description": "LOT 번호", + "type": "string", + "example": "RM260319", + "nullable": true }, "item_name": { "type": "string", @@ -36518,6 +36524,502 @@ ] } }, + "/api/v1/org-chart": { + "get": { + "tags": [ + "OrgChart" + ], + "summary": "조직도 전체 조회 (부서 트리 + 직원 + 통계)", + "operationId": "d92f1fc2da2664ef3543e4e1328d25e9", + "parameters": [ + { + "name": "include_hidden", + "in": "query", + "description": "숨겨진 부서 포함 여부 (기본: true)", + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "description": "성공", + "content": { + "application/json": { + "schema": { + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "message": { + "type": "string" + }, + "data": { + "properties": { + "company": { + "properties": { + "name": { + "type": "string", + "example": "주일산업" + }, + "ceo_name": { + "type": "string", + "example": "홍길동" + } + }, + "type": "object" + }, + "departments": { + "type": "array", + "items": { + "$ref": "#/components/schemas/OrgChartDepartmentNode" + } + }, + "hidden_departments": { + "type": "array", + "items": { + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "code": { + "type": "string" + } + }, + "type": "object" + } + }, + "unassigned": { + "type": "array", + "items": { + "$ref": "#/components/schemas/OrgChartEmployee" + } + }, + "stats": { + "$ref": "#/components/schemas/OrgChartStats" + } + }, + "type": "object" + } + }, + "type": "object" + } + } + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "BearerAuth": [] + } + ] + } + }, + "/api/v1/org-chart/stats": { + "get": { + "tags": [ + "OrgChart" + ], + "summary": "조직도 통계 (전체/배치/미배치 인원)", + "operationId": "c00470fa81707bdf862c9eec4139d0ba", + "responses": { + "200": { + "description": "성공", + "content": { + "application/json": { + "schema": { + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "message": { + "type": "string" + }, + "data": { + "$ref": "#/components/schemas/OrgChartStats" + } + }, + "type": "object" + } + } + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "BearerAuth": [] + } + ] + } + }, + "/api/v1/org-chart/unassigned": { + "get": { + "tags": [ + "OrgChart" + ], + "summary": "미배치 직원 목록", + "operationId": "13121a2e9ccefcfbf7cc60c727522bc4", + "parameters": [ + { + "name": "q", + "in": "query", + "description": "이름 검색", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "성공", + "content": { + "application/json": { + "schema": { + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "message": { + "type": "string" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/OrgChartEmployee" + } + } + }, + "type": "object" + } + } + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "BearerAuth": [] + } + ] + } + }, + "/api/v1/org-chart/assign": { + "post": { + "tags": [ + "OrgChart" + ], + "summary": "직원 부서 배치", + "operationId": "6a7281807e859f10409469dda17fdd89", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrgChartAssignRequest" + } + } + } + }, + "responses": { + "200": { + "description": "성공", + "content": { + "application/json": { + "schema": { + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "message": { + "type": "string" + }, + "data": { + "properties": { + "employee_id": { + "type": "integer" + }, + "department_id": { + "type": "integer" + } + }, + "type": "object" + } + }, + "type": "object" + } + } + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "BearerAuth": [] + } + ] + } + }, + "/api/v1/org-chart/unassign": { + "post": { + "tags": [ + "OrgChart" + ], + "summary": "직원 미배치 처리", + "operationId": "387e1c87037f9bf708b7255c35f5e86c", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrgChartUnassignRequest" + } + } + } + }, + "responses": { + "200": { + "description": "성공", + "content": { + "application/json": { + "schema": { + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "message": { + "type": "string" + }, + "data": { + "properties": { + "employee_id": { + "type": "integer" + }, + "department_id": { + "type": "integer", + "nullable": true + } + }, + "type": "object" + } + }, + "type": "object" + } + } + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "BearerAuth": [] + } + ] + } + }, + "/api/v1/org-chart/reorder-employees": { + "put": { + "tags": [ + "OrgChart" + ], + "summary": "직원 일괄 배치/이동", + "operationId": "7af302b074864287d8f4b09af4e27c9c", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrgChartReorderEmployeesRequest" + } + } + } + }, + "responses": { + "200": { + "description": "성공", + "content": { + "application/json": { + "schema": { + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "message": { + "type": "string" + }, + "data": { + "properties": { + "processed": { + "type": "integer", + "example": 3 + } + }, + "type": "object" + } + }, + "type": "object" + } + } + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "BearerAuth": [] + } + ] + } + }, + "/api/v1/org-chart/reorder-departments": { + "put": { + "tags": [ + "OrgChart" + ], + "summary": "부서 순서/계층 일괄 변경", + "operationId": "da0aa294c8ebf3ab4f7550f56ca901f3", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrgChartReorderDepartmentsRequest" + } + } + } + }, + "responses": { + "200": { + "description": "성공", + "content": { + "application/json": { + "schema": { + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "message": { + "type": "string" + }, + "data": { + "properties": { + "processed": { + "type": "integer", + "example": 5 + } + }, + "type": "object" + } + }, + "type": "object" + } + } + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "BearerAuth": [] + } + ] + } + }, + "/api/v1/org-chart/departments/{id}/toggle-hide": { + "patch": { + "tags": [ + "OrgChart" + ], + "summary": "부서 숨기기/표시 토글", + "operationId": "b3913a80123a5bc5b8f7e4d07beed9c8", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "부서 ID", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrgChartToggleHideRequest" + } + } + } + }, + "responses": { + "200": { + "description": "성공", + "content": { + "application/json": { + "schema": { + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "message": { + "type": "string" + }, + "data": { + "properties": { + "id": { + "type": "integer" + }, + "orgchart_hidden": { + "type": "boolean" + } + }, + "type": "object" + } + }, + "type": "object" + } + } + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "BearerAuth": [] + } + ] + } + }, "/api/v1/payments": { "get": { "tags": [ @@ -49474,14 +49976,9 @@ { "name": "delivery_method", "in": "query", - "description": "배송방식", + "description": "배송방식 (common_codes delivery_method 참조)", "schema": { - "type": "string", - "enum": [ - "pickup", - "direct", - "logistics" - ] + "type": "string" } }, { @@ -64904,9 +65401,15 @@ "example": 431 }, "code": { - "description": "LOT 코드: {제품Code}{종류Code}{YYMMDD}", + "description": "코드 체계 (제품Code+종류Code)", "type": "string", - "example": "RS260319" + "example": "RS" + }, + "lot_no": { + "description": "LOT 번호 (code+날짜+일련번호)", + "type": "string", + "example": "RS260319", + "nullable": true }, "legacy_code": { "description": "이전 코드 (items 기반)", @@ -80193,6 +80696,204 @@ }, "type": "object" }, + "OrgChartEmployee": { + "properties": { + "id": { + "description": "프로필 ID", + "type": "integer", + "example": 5 + }, + "user_id": { + "type": "integer", + "example": 3 + }, + "department_id": { + "type": "integer", + "example": 1, + "nullable": true + }, + "display_name": { + "type": "string", + "example": "홍길동" + }, + "position_label": { + "type": "string", + "example": "과장", + "nullable": true + } + }, + "type": "object" + }, + "OrgChartDepartmentNode": { + "properties": { + "id": { + "type": "integer", + "example": 1 + }, + "name": { + "type": "string", + "example": "경영지원팀" + }, + "code": { + "type": "string", + "example": "MGT", + "nullable": true + }, + "parent_id": { + "type": "integer", + "nullable": true + }, + "sort_order": { + "type": "integer", + "example": 1 + }, + "is_active": { + "type": "boolean", + "example": true + }, + "orgchart_hidden": { + "type": "boolean", + "example": false + }, + "children": { + "type": "array", + "items": { + "$ref": "#/components/schemas/OrgChartDepartmentNode" + } + }, + "employees": { + "type": "array", + "items": { + "$ref": "#/components/schemas/OrgChartEmployee" + } + } + }, + "type": "object" + }, + "OrgChartStats": { + "properties": { + "total": { + "type": "integer", + "example": 50 + }, + "assigned": { + "type": "integer", + "example": 42 + }, + "unassigned": { + "type": "integer", + "example": 8 + } + }, + "type": "object" + }, + "OrgChartAssignRequest": { + "required": [ + "employee_id", + "department_id" + ], + "properties": { + "employee_id": { + "description": "직원 프로필 ID", + "type": "integer", + "example": 5 + }, + "department_id": { + "description": "배치할 부서 ID", + "type": "integer", + "example": 3 + } + }, + "type": "object" + }, + "OrgChartUnassignRequest": { + "required": [ + "employee_id" + ], + "properties": { + "employee_id": { + "description": "직원 프로필 ID", + "type": "integer", + "example": 5 + } + }, + "type": "object" + }, + "OrgChartReorderEmployeesRequest": { + "required": [ + "moves" + ], + "properties": { + "moves": { + "type": "array", + "items": { + "required": [ + "employee_id" + ], + "properties": { + "employee_id": { + "type": "integer", + "example": 5 + }, + "department_id": { + "type": "integer", + "example": 3, + "nullable": true + } + }, + "type": "object" + } + } + }, + "type": "object" + }, + "OrgChartReorderDepartmentsRequest": { + "required": [ + "orders" + ], + "properties": { + "orders": { + "type": "array", + "items": { + "required": [ + "id", + "parent_id", + "sort_order" + ], + "properties": { + "id": { + "type": "integer", + "example": 1 + }, + "parent_id": { + "type": "integer", + "example": null, + "nullable": true + }, + "sort_order": { + "type": "integer", + "example": 0 + } + }, + "type": "object" + } + } + }, + "type": "object" + }, + "OrgChartToggleHideRequest": { + "required": [ + "hidden" + ], + "properties": { + "hidden": { + "description": "숨기기 여부", + "type": "boolean", + "example": true + } + }, + "type": "object" + }, "Payment": { "description": "결제 정보", "properties": { @@ -87955,13 +88656,8 @@ "example": "보통" }, "delivery_method": { - "description": "배송방식", + "description": "배송방식 (common_codes delivery_method 참조)", "type": "string", - "enum": [ - "pickup", - "direct", - "logistics" - ], "example": "pickup" }, "delivery_method_label": { @@ -88371,13 +89067,8 @@ "example": "normal" }, "delivery_method": { - "description": "배송방식", + "description": "배송방식 (common_codes delivery_method 참조)", "type": "string", - "enum": [ - "pickup", - "direct", - "logistics" - ], "example": "pickup" }, "client_id": { @@ -95014,6 +95705,10 @@ "name": "Order", "description": "수주관리 API" }, + { + "name": "OrgChart", + "description": "조직도 관리" + }, { "name": "Payments", "description": "결제 관리"