feat: 파일 저장 시스템 DB 마이그레이션

- enhance_files_table: 이중 파일명 시스템 (display_name/stored_name), 폴더 관리, 문서 연결 지원
- create_folders_table: 동적 폴더 관리 시스템 (tenant별 커스터마이징 가능)
- 5개 stub 마이그레이션 생성 (file_share_links, file_deletion_logs, storage_usage_history, add_storage_columns_to_tenants)
- FolderSeeder stub 생성
- CURRENT_WORKS.md에 Phase 1 진행상황 문서화

fix: 파일 공유 및 삭제 기능 버그 수정

- ShareLinkRequest: PATH 파라미터 {id}를 file_id로 자동 병합
- routes/api.php: 공유 링크 다운로드를 auth.apikey 그룹 밖으로 이동 (인증 불필요)
- FileShareLink: File, Tenant 클래스 import 추가
- File 모델: softDeleteFile()에서 SoftDeletes의 delete() 메서드 사용
- FileStorageService: getTrash(), restoreFile(), permanentDelete()에서 onlyTrashed() 사용
- File 모델: Tenant 네임스페이스 수정 (App\Models\Tenants\Tenant)

refactor: Swagger 문서 정리 - File 태그를 Files로 통합

- FileApi.php의 모든 태그를 Files로 변경
- 구 파일 시스템 라우트 삭제 (prefix 'file')
- 구 FileController.php 삭제
- 신규 파일 저장소 시스템으로 완전 통합

fix: 모든 legacy 파일 컬럼 nullable 일괄 처리

- 5개 legacy 컬럼을 한 번에 nullable로 변경
  * original_name, file_name, file_name_old (string)
  * fileable_id, fileable_type (polymorphic)
- foreach 루프로 반복 작업 자동화
- 신규/기존 시스템 간 완전한 하위 호환성 확보

fix: legacy 파일 컬럼 nullable 처리 완료

- file_name, file_name_old 컬럼도 nullable로 변경
- 기존 시스템과 신규 시스템 간 완전한 하위 호환성 확보
- Legacy: original_name, file_name, file_name_old (nullable)
- New: display_name, stored_name (required)

fix: original_name 컬럼 nullable 처리

- original_name을 nullable로 변경하여 하위 호환성 유지
- 새 시스템에서는 display_name 사용, 기존 시스템은 original_name 사용 가능

fix: 파일 업로드 DB 컬럼 누락 및 메시지 구조 개선

- files 테이블에 감사 컬럼 추가 (created_by, updated_by, uploaded_by)
- ApiResponse::handle() 메시지 로직 개선 (접미사 제거)
- 다국어 지원을 위한 완성된 문장 구조 유지
- FileUploadRequest 파일 검증 규칙 수정

fix: 파일 저장소 버그 수정 및 신규 테넌트 폴더 자동 생성

- FolderSeeder 네임스페이스 수정 (App\Models\Tenant → App\Models\Tenants\Tenant)
- FileStorageController use 문 구문 오류 수정 (/ → \)
- TenantObserver에 신규 테넌트 기본 폴더 자동 생성 로직 추가
  - 5개 기본 폴더 (생산관리, 품질관리, 회계, 인사, 일반)
  - 에러 처리 및 로깅
  - 회원가입 시 자동 실행
This commit is contained in:
2025-11-10 19:08:56 +09:00
parent dbe3ed698a
commit c83e029448
64 changed files with 3960 additions and 349 deletions

View File

@@ -2,4 +2,529 @@
namespace App\Swagger\v1;
class FileApi {}
use OpenApi\Annotations as OA;
/**
* @OA\Tag(name="Files", description="파일 저장소 관리")
*
* ========= 스키마 정의 =========
*
* @OA\Schema(
* schema="File",
* type="object",
* description="파일 모델",
*
* @OA\Property(property="id", type="integer", example=1),
* @OA\Property(property="tenant_id", type="integer", example=1),
* @OA\Property(property="display_name", type="string", example="계약서.pdf", description="사용자에게 표시되는 파일명"),
* @OA\Property(property="stored_name", type="string", example="a1b2c3d4e5f6g7h8.pdf", description="실제 저장된 파일명"),
* @OA\Property(property="folder_id", type="integer", nullable=true, example=1),
* @OA\Property(property="is_temp", type="boolean", example=false),
* @OA\Property(property="file_path", type="string", example="1/product/2025/01/a1b2c3d4e5f6g7h8.pdf"),
* @OA\Property(property="file_size", type="integer", example=1024000, description="파일 크기 (bytes)"),
* @OA\Property(property="mime_type", type="string", example="application/pdf"),
* @OA\Property(property="file_type", type="string", enum={"document","image","excel","archive"}, example="document"),
* @OA\Property(property="document_id", type="integer", nullable=true, example=null),
* @OA\Property(property="document_type", type="string", nullable=true, example=null),
* @OA\Property(property="uploaded_by", type="integer", example=1),
* @OA\Property(property="deleted_by", type="integer", nullable=true, example=null),
* @OA\Property(property="created_at", type="string", format="date-time", example="2025-01-01T00:00:00Z"),
* @OA\Property(property="updated_at", type="string", format="date-time", example="2025-01-01T00:00:00Z"),
* @OA\Property(property="deleted_at", type="string", format="date-time", nullable=true, example=null)
* )
*
* @OA\Schema(
* schema="FileShareLink",
* type="object",
* description="파일 공유 링크",
*
* @OA\Property(property="token", type="string", example="a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6", description="64자 공유 토큰"),
* @OA\Property(property="url", type="string", example="http://api.sam.kr/api/v1/files/share/a1b2c3d4"),
* @OA\Property(property="expires_at", type="string", format="date-time", example="2025-01-02T00:00:00Z")
* )
*
* @OA\Schema(
* schema="StorageUsage",
* type="object",
* description="저장소 사용량 정보",
*
* @OA\Property(property="storage_limit", type="integer", example=10737418240, description="저장소 한도 (bytes)"),
* @OA\Property(property="storage_used", type="integer", example=5368709120, description="사용 중인 용량 (bytes)"),
* @OA\Property(property="storage_used_formatted", type="string", example="5.00 GB"),
* @OA\Property(property="storage_limit_formatted", type="string", example="10.00 GB"),
* @OA\Property(property="usage_percentage", type="number", format="float", example=50.0),
* @OA\Property(property="file_count", type="integer", example=150),
* @OA\Property(
* property="folder_breakdown",
* type="object",
* description="폴더별 사용량",
* example={"product": 2147483648, "quality": 1073741824, "accounting": 536870912}
* )
* )
*/
class FileApi
{
/**
* 파일 업로드 (임시 폴더)
*
* @OA\Post(
* path="/api/v1/files/upload",
* summary="파일 업로드",
* description="파일을 임시 폴더에 업로드합니다. 이후 /files/move로 정식 폴더로 이동시켜야 합니다.",
* tags={"Files"},
* security={{"ApiKeyAuth": {}},{"BearerAuth": {}}},
*
* @OA\RequestBody(
* required=true,
*
* @OA\MediaType(
* mediaType="multipart/form-data",
*
* @OA\Schema(
* required={"file"},
*
* @OA\Property(property="file", type="string", format="binary", description="업로드할 파일 (최대 20MB)"),
* @OA\Property(property="description", type="string", nullable=true, maxLength=500, example="계약서 원본")
* )
* )
* ),
*
* @OA\Response(
* response=200,
* description="파일 업로드 성공",
*
* @OA\JsonContent(
* allOf={
*
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
* @OA\Schema(
*
* @OA\Property(property="data", ref="#/components/schemas/File")
* )
* }
* )
* ),
*
* @OA\Response(response=400, description="용량 초과 또는 파일 형식 오류"),
* @OA\Response(response=401, description="인증 실패")
* )
*/
public function upload() {}
/**
* 파일 이동 (temp → folder)
*
* @OA\Post(
* path="/api/v1/files/move",
* summary="파일 이동",
* description="임시 폴더의 파일들을 정식 폴더로 이동하고 문서에 첨부합니다.",
* tags={"Files"},
* security={{"ApiKeyAuth": {}},{"BearerAuth": {}}},
*
* @OA\RequestBody(
* required=true,
*
* @OA\JsonContent(
* required={"file_ids","folder_id"},
*
* @OA\Property(property="file_ids", type="array", @OA\Items(type="integer"), example={1,2,3}),
* @OA\Property(property="folder_id", type="integer", example=1, description="대상 폴더 ID"),
* @OA\Property(property="document_id", type="integer", nullable=true, example=10, description="첨부할 문서 ID"),
* @OA\Property(property="document_type", type="string", nullable=true, maxLength=100, example="Order", description="문서 타입")
* )
* ),
*
* @OA\Response(
* response=200,
* description="파일 이동 성공",
*
* @OA\JsonContent(
* allOf={
*
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
* @OA\Schema(
*
* @OA\Property(property="data", type="array", @OA\Items(ref="#/components/schemas/File"))
* )
* }
* )
* ),
*
* @OA\Response(response=404, description="파일 또는 폴더를 찾을 수 없음")
* )
*/
public function move() {}
/**
* 파일 목록 조회
*
* @OA\Get(
* path="/api/v1/files",
* summary="파일 목록 조회",
* description="폴더별, 문서별 파일 목록을 조회합니다.",
* tags={"Files"},
* security={{"ApiKeyAuth": {}},{"BearerAuth": {}}},
*
* @OA\Parameter(
* name="folder_id",
* in="query",
* description="폴더 ID (선택)",
*
* @OA\Schema(type="integer")
* ),
*
* @OA\Parameter(
* name="document_id",
* in="query",
* description="문서 ID (선택)",
*
* @OA\Schema(type="integer")
* ),
*
* @OA\Parameter(
* name="document_type",
* in="query",
* description="문서 타입 (선택)",
*
* @OA\Schema(type="string")
* ),
*
* @OA\Parameter(
* name="is_temp",
* in="query",
* description="임시 파일만 조회",
*
* @OA\Schema(type="boolean")
* ),
*
* @OA\Response(
* response=200,
* description="파일 목록 조회 성공",
*
* @OA\JsonContent(
* allOf={
*
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
* @OA\Schema(
*
* @OA\Property(property="data", type="array", @OA\Items(ref="#/components/schemas/File"))
* )
* }
* )
* )
* )
*/
public function index() {}
/**
* 파일 상세 조회
*
* @OA\Get(
* path="/api/v1/files/{id}",
* summary="파일 상세 조회",
* description="파일 ID로 상세 정보를 조회합니다.",
* tags={"Files"},
* security={{"ApiKeyAuth": {}},{"BearerAuth": {}}},
*
* @OA\Parameter(
* name="id",
* in="path",
* required=true,
*
* @OA\Schema(type="integer")
* ),
*
* @OA\Response(
* response=200,
* description="파일 조회 성공",
*
* @OA\JsonContent(
* allOf={
*
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
* @OA\Schema(
*
* @OA\Property(property="data", ref="#/components/schemas/File")
* )
* }
* )
* ),
*
* @OA\Response(response=404, description="파일을 찾을 수 없음")
* )
*/
public function show() {}
/**
* 휴지통 파일 목록
*
* @OA\Get(
* path="/api/v1/files/trash",
* summary="휴지통 파일 목록",
* description="삭제된 파일 목록을 조회합니다 (30일 보관).",
* tags={"Files"},
* security={{"ApiKeyAuth": {}},{"BearerAuth": {}}},
*
* @OA\Response(
* response=200,
* description="휴지통 조회 성공",
*
* @OA\JsonContent(
* allOf={
*
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
* @OA\Schema(
*
* @OA\Property(property="data", type="array", @OA\Items(ref="#/components/schemas/File"))
* )
* }
* )
* )
* )
*/
public function trash() {}
/**
* 파일 다운로드
*
* @OA\Get(
* path="/api/v1/files/{id}/download",
* summary="파일 다운로드",
* description="파일을 다운로드합니다.",
* tags={"Files"},
* security={{"ApiKeyAuth": {}},{"BearerAuth": {}}},
*
* @OA\Parameter(
* name="id",
* in="path",
* required=true,
*
* @OA\Schema(type="integer")
* ),
*
* @OA\Response(
* response=200,
* description="파일 다운로드",
*
* @OA\MediaType(
* mediaType="application/octet-stream",
*
* @OA\Schema(type="string", format="binary")
* )
* ),
*
* @OA\Response(response=404, description="파일을 찾을 수 없음")
* )
*/
public function download() {}
/**
* 파일 삭제 (soft delete)
*
* @OA\Delete(
* path="/api/v1/files/{id}",
* summary="파일 삭제",
* description="파일을 휴지통으로 이동합니다 (복구 가능).",
* tags={"Files"},
* security={{"ApiKeyAuth": {}},{"BearerAuth": {}}},
*
* @OA\Parameter(
* name="id",
* in="path",
* required=true,
*
* @OA\Schema(type="integer")
* ),
*
* @OA\Response(
* response=200,
* description="파일 삭제 성공",
*
* @OA\JsonContent(ref="#/components/schemas/ApiResponse")
* ),
*
* @OA\Response(response=404, description="파일을 찾을 수 없음")
* )
*/
public function destroy() {}
/**
* 파일 복구
*
* @OA\Post(
* path="/api/v1/files/{id}/restore",
* summary="파일 복구",
* description="휴지통의 파일을 복구합니다.",
* tags={"Files"},
* security={{"ApiKeyAuth": {}},{"BearerAuth": {}}},
*
* @OA\Parameter(
* name="id",
* in="path",
* required=true,
*
* @OA\Schema(type="integer")
* ),
*
* @OA\Response(
* response=200,
* description="파일 복구 성공",
*
* @OA\JsonContent(
* allOf={
*
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
* @OA\Schema(
*
* @OA\Property(property="data", ref="#/components/schemas/File")
* )
* }
* )
* ),
*
* @OA\Response(response=404, description="파일을 찾을 수 없음")
* )
*/
public function restore() {}
/**
* 파일 영구 삭제
*
* @OA\Delete(
* path="/api/v1/files/{id}/permanent",
* summary="파일 영구 삭제",
* description="파일을 물리적으로 완전히 삭제합니다 (복구 불가).",
* tags={"Files"},
* security={{"ApiKeyAuth": {}},{"BearerAuth": {}}},
*
* @OA\Parameter(
* name="id",
* in="path",
* required=true,
*
* @OA\Schema(type="integer")
* ),
*
* @OA\Response(
* response=200,
* description="파일 영구 삭제 성공",
*
* @OA\JsonContent(ref="#/components/schemas/ApiResponse")
* ),
*
* @OA\Response(response=404, description="파일을 찾을 수 없음")
* )
*/
public function permanentDelete() {}
/**
* 공유 링크 생성
*
* @OA\Post(
* path="/api/v1/files/{id}/share",
* summary="공유 링크 생성",
* description="파일의 임시 공유 링크를 생성합니다 (기본 24시간).",
* tags={"Files"},
* security={{"ApiKeyAuth": {}},{"BearerAuth": {}}},
*
* @OA\Parameter(
* name="id",
* in="path",
* required=true,
*
* @OA\Schema(type="integer")
* ),
*
* @OA\RequestBody(
*
* @OA\JsonContent(
*
* @OA\Property(property="expiry_hours", type="integer", minimum=1, maximum=168, example=24, description="만료 시간 (시간 단위, 최대 7일)")
* )
* ),
*
* @OA\Response(
* response=200,
* description="공유 링크 생성 성공",
*
* @OA\JsonContent(
* allOf={
*
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
* @OA\Schema(
*
* @OA\Property(property="data", ref="#/components/schemas/FileShareLink")
* )
* }
* )
* ),
*
* @OA\Response(response=404, description="파일을 찾을 수 없음")
* )
*/
public function createShareLink() {}
/**
* 공유 링크로 다운로드 (인증 불필요)
*
* @OA\Get(
* path="/api/v1/files/share/{token}",
* summary="공유 파일 다운로드",
* description="공유 토큰으로 파일을 다운로드합니다 (인증 불필요).",
* tags={"Files"},
*
* @OA\Parameter(
* name="token",
* in="path",
* required=true,
* description="64자 공유 토큰",
*
* @OA\Schema(type="string")
* ),
*
* @OA\Response(
* response=200,
* description="파일 다운로드",
*
* @OA\MediaType(
* mediaType="application/octet-stream",
*
* @OA\Schema(type="string", format="binary")
* )
* ),
*
* @OA\Response(response=404, description="토큰을 찾을 수 없음"),
* @OA\Response(response=410, description="링크가 만료됨")
* )
*/
public function downloadShared() {}
/**
* 저장소 사용량 조회
*
* @OA\Get(
* path="/api/v1/storage/usage",
* summary="저장소 사용량 조회",
* description="현재 테넌트의 저장소 사용량 정보를 조회합니다.",
* tags={"Files"},
* security={{"ApiKeyAuth": {}},{"BearerAuth": {}}},
*
* @OA\Response(
* response=200,
* description="사용량 조회 성공",
*
* @OA\JsonContent(
* allOf={
*
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
* @OA\Schema(
*
* @OA\Property(property="data", ref="#/components/schemas/StorageUsage")
* )
* }
* )
* )
* )
*/
public function storageUsage() {}
}

View File

@@ -0,0 +1,324 @@
<?php
namespace App\Swagger\v1;
use OpenApi\Annotations as OA;
/**
* @OA\Tag(name="Folder", description="폴더 관리")
*
* ========= 스키마 정의 =========
*
* @OA\Schema(
* schema="Folder",
* type="object",
* description="폴더 모델",
*
* @OA\Property(property="id", type="integer", example=1),
* @OA\Property(property="tenant_id", type="integer", example=1),
* @OA\Property(property="folder_key", type="string", example="product", description="폴더 키 (영문 소문자, 숫자, 하이픈, 언더스코어만 허용)"),
* @OA\Property(property="folder_name", type="string", example="생산관리", description="폴더 표시명"),
* @OA\Property(property="description", type="string", nullable=true, example="생산 관련 문서", maxLength=500),
* @OA\Property(property="display_order", type="integer", example=1, description="정렬 순서"),
* @OA\Property(property="is_active", type="boolean", example=true),
* @OA\Property(property="icon", type="string", nullable=true, example="icon-production", maxLength=50),
* @OA\Property(property="color", type="string", nullable=true, example="#3B82F6", description="색상 코드 (#RRGGBB 형식)"),
* @OA\Property(property="created_by", type="integer", example=1),
* @OA\Property(property="updated_by", type="integer", nullable=true, example=1),
* @OA\Property(property="created_at", type="string", format="date-time", example="2025-01-01T00:00:00Z"),
* @OA\Property(property="updated_at", type="string", format="date-time", example="2025-01-01T00:00:00Z")
* )
*
* @OA\Schema(
* schema="FolderStoreRequest",
* type="object",
* required={"folder_key", "folder_name"},
* description="폴더 생성 요청",
*
* @OA\Property(property="folder_key", type="string", pattern="^[a-z0-9_-]+$", maxLength=50, example="accounting", description="폴더 키 (영문 소문자, 숫자, 하이픈, 언더스코어만)"),
* @OA\Property(property="folder_name", type="string", maxLength=100, example="회계", description="폴더 표시명"),
* @OA\Property(property="description", type="string", nullable=true, maxLength=500, example="회계 관련 문서"),
* @OA\Property(property="display_order", type="integer", nullable=true, minimum=0, example=10, description="정렬 순서 (미지정 시 자동)"),
* @OA\Property(property="is_active", type="boolean", nullable=true, example=true),
* @OA\Property(property="icon", type="string", nullable=true, maxLength=50, example="icon-accounting"),
* @OA\Property(property="color", type="string", nullable=true, pattern="^#[0-9A-Fa-f]{6}$", example="#10B981")
* )
*
* @OA\Schema(
* schema="FolderUpdateRequest",
* type="object",
* description="폴더 수정 요청",
*
* @OA\Property(property="folder_key", type="string", pattern="^[a-z0-9_-]+$", maxLength=50, example="accounting"),
* @OA\Property(property="folder_name", type="string", maxLength=100, example="회계"),
* @OA\Property(property="description", type="string", nullable=true, maxLength=500, example="회계 관련 문서"),
* @OA\Property(property="display_order", type="integer", minimum=0, example=5),
* @OA\Property(property="is_active", type="boolean", example=true),
* @OA\Property(property="icon", type="string", nullable=true, maxLength=50, example="icon-accounting"),
* @OA\Property(property="color", type="string", nullable=true, pattern="^#[0-9A-Fa-f]{6}$", example="#10B981")
* )
*
* @OA\Schema(
* schema="FolderReorderRequest",
* type="object",
* required={"orders"},
* description="폴더 순서 변경 요청",
*
* @OA\Property(
* property="orders",
* type="array",
* description="폴더 ID와 순서 배열",
*
* @OA\Items(
* type="object",
* required={"id", "display_order"},
*
* @OA\Property(property="id", type="integer", example=1),
* @OA\Property(property="display_order", type="integer", example=0)
* ),
* example={
* {"id": 1, "display_order": 0},
* {"id": 2, "display_order": 1},
* {"id": 3, "display_order": 2}
* }
* )
* )
*/
class FolderApi
{
/**
* 폴더 목록 조회
*
* @OA\Get(
* path="/api/v1/folders",
* summary="폴더 목록 조회",
* description="테넌트의 모든 폴더를 display_order 순으로 조회합니다.",
* tags={"Folder"},
* security={{"ApiKeyAuth": {}},{"BearerAuth": {}}},
*
* @OA\Response(
* response=200,
* description="폴더 목록 조회 성공",
*
* @OA\JsonContent(
* allOf={
*
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
* @OA\Schema(
*
* @OA\Property(property="data", type="array", @OA\Items(ref="#/components/schemas/Folder"))
* )
* }
* )
* ),
*
* @OA\Response(response=401, description="인증 실패")
* )
*/
public function index() {}
/**
* 폴더 생성
*
* @OA\Post(
* path="/api/v1/folders",
* summary="폴더 생성",
* description="새로운 폴더를 생성합니다.",
* tags={"Folder"},
* security={{"ApiKeyAuth": {}},{"BearerAuth": {}}},
*
* @OA\RequestBody(
* required=true,
*
* @OA\JsonContent(ref="#/components/schemas/FolderStoreRequest")
* ),
*
* @OA\Response(
* response=200,
* description="폴더 생성 성공",
*
* @OA\JsonContent(
* allOf={
*
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
* @OA\Schema(
*
* @OA\Property(property="data", ref="#/components/schemas/Folder")
* )
* }
* )
* ),
*
* @OA\Response(response=400, description="유효성 검증 실패"),
* @OA\Response(response=409, description="중복된 folder_key")
* )
*/
public function store() {}
/**
* 폴더 상세 조회
*
* @OA\Get(
* path="/api/v1/folders/{id}",
* summary="폴더 상세 조회",
* description="폴더 ID로 상세 정보를 조회합니다.",
* tags={"Folder"},
* security={{"ApiKeyAuth": {}},{"BearerAuth": {}}},
*
* @OA\Parameter(
* name="id",
* in="path",
* required=true,
* description="폴더 ID",
*
* @OA\Schema(type="integer")
* ),
*
* @OA\Response(
* response=200,
* description="폴더 조회 성공",
*
* @OA\JsonContent(
* allOf={
*
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
* @OA\Schema(
*
* @OA\Property(property="data", ref="#/components/schemas/Folder")
* )
* }
* )
* ),
*
* @OA\Response(response=404, description="폴더를 찾을 수 없음")
* )
*/
public function show() {}
/**
* 폴더 수정
*
* @OA\Put(
* path="/api/v1/folders/{id}",
* summary="폴더 수정",
* description="폴더 정보를 수정합니다.",
* tags={"Folder"},
* security={{"ApiKeyAuth": {}},{"BearerAuth": {}}},
*
* @OA\Parameter(
* name="id",
* in="path",
* required=true,
* description="폴더 ID",
*
* @OA\Schema(type="integer")
* ),
*
* @OA\RequestBody(
* required=true,
*
* @OA\JsonContent(ref="#/components/schemas/FolderUpdateRequest")
* ),
*
* @OA\Response(
* response=200,
* description="폴더 수정 성공",
*
* @OA\JsonContent(
* allOf={
*
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
* @OA\Schema(
*
* @OA\Property(property="data", ref="#/components/schemas/Folder")
* )
* }
* )
* ),
*
* @OA\Response(response=400, description="유효성 검증 실패"),
* @OA\Response(response=404, description="폴더를 찾을 수 없음"),
* @OA\Response(response=409, description="중복된 folder_key")
* )
*/
public function update() {}
/**
* 폴더 삭제/비활성화
*
* @OA\Delete(
* path="/api/v1/folders/{id}",
* summary="폴더 삭제",
* description="폴더를 비활성화합니다. 파일이 있는 폴더는 삭제할 수 없습니다.",
* tags={"Folder"},
* security={{"ApiKeyAuth": {}},{"BearerAuth": {}}},
*
* @OA\Parameter(
* name="id",
* in="path",
* required=true,
* description="폴더 ID",
*
* @OA\Schema(type="integer")
* ),
*
* @OA\Response(
* response=200,
* description="폴더 삭제 성공",
*
* @OA\JsonContent(
* allOf={
*
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
* @OA\Schema(
*
* @OA\Property(property="data", ref="#/components/schemas/Folder")
* )
* }
* )
* ),
*
* @OA\Response(response=400, description="폴더에 파일이 있어 삭제 불가"),
* @OA\Response(response=404, description="폴더를 찾을 수 없음")
* )
*/
public function destroy() {}
/**
* 폴더 순서 변경
*
* @OA\Post(
* path="/api/v1/folders/reorder",
* summary="폴더 순서 변경",
* description="여러 폴더의 표시 순서를 일괄 변경합니다.",
* tags={"Folder"},
* security={{"ApiKeyAuth": {}},{"BearerAuth": {}}},
*
* @OA\RequestBody(
* required=true,
*
* @OA\JsonContent(ref="#/components/schemas/FolderReorderRequest")
* ),
*
* @OA\Response(
* response=200,
* description="순서 변경 성공",
*
* @OA\JsonContent(
* allOf={
*
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
* @OA\Schema(
*
* @OA\Property(property="data", type="array", @OA\Items(ref="#/components/schemas/Folder"))
* )
* }
* )
* ),
*
* @OA\Response(response=400, description="유효성 검증 실패"),
* @OA\Response(response=404, description="일부 폴더를 찾을 수 없음")
* )
*/
public function reorder() {}
}

View File

@@ -63,4 +63,4 @@ class RefreshApi
* )
*/
public function refresh() {}
}
}

View File

@@ -77,8 +77,10 @@
* property="menus",
* type="array",
* description="생성된 테넌트의 기본 메뉴 목록 (9개: 대시보드, 기초정보관리, 제품관리, 거래처관리, BOM관리, 시스템관리, 사용자관리, 권한관리, 부서관리)",
*
* @OA\Items(
* type="object",
*
* @OA\Property(property="id", type="integer", example=1, description="메뉴 ID"),
* @OA\Property(property="parent_id", type="integer", example=null, nullable=true, description="상위 메뉴 ID (최상위 메뉴는 null)"),
* @OA\Property(property="name", type="string", example="대시보드", description="메뉴명"),