2025-07-17 10:05:47 +09:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
namespace App\Helpers;
|
|
|
|
|
|
2025-12-11 19:01:07 +09:00
|
|
|
use App\Exceptions\DuplicateCodeException;
|
2025-07-17 10:05:47 +09:00
|
|
|
use Illuminate\Http\JsonResponse;
|
2025-11-06 17:45:49 +09:00
|
|
|
use Illuminate\Support\Facades\DB;
|
2026-02-07 03:27:07 +09:00
|
|
|
use Illuminate\Support\Facades\Log;
|
2025-08-20 20:23:01 +09:00
|
|
|
use Symfony\Component\HttpKernel\Exception\HttpException;
|
2025-07-17 10:05:47 +09:00
|
|
|
|
|
|
|
|
class ApiResponse
|
|
|
|
|
{
|
2025-12-24 19:48:15 +09:00
|
|
|
/**
|
|
|
|
|
* ISO 8601 날짜를 Y-m-d 형식으로 변환
|
|
|
|
|
*
|
|
|
|
|
* @param mixed $data 변환할 데이터 (배열, 객체, 페이지네이션 등)
|
|
|
|
|
* @return mixed 변환된 데이터
|
|
|
|
|
*/
|
|
|
|
|
public static function formatDates(mixed $data): mixed
|
|
|
|
|
{
|
|
|
|
|
// null이면 그대로 반환
|
|
|
|
|
if ($data === null) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Paginator 처리
|
|
|
|
|
if ($data instanceof \Illuminate\Pagination\LengthAwarePaginator ||
|
|
|
|
|
$data instanceof \Illuminate\Pagination\Paginator) {
|
|
|
|
|
$items = self::formatDates($data->items());
|
|
|
|
|
$data->setCollection(collect($items));
|
|
|
|
|
|
|
|
|
|
return $data;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Collection 처리
|
|
|
|
|
if ($data instanceof \Illuminate\Support\Collection) {
|
|
|
|
|
return $data->map(fn ($item) => self::formatDates($item));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Model 처리
|
|
|
|
|
if ($data instanceof \Illuminate\Database\Eloquent\Model) {
|
|
|
|
|
return self::formatDates($data->toArray());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 배열 처리
|
|
|
|
|
if (is_array($data)) {
|
|
|
|
|
foreach ($data as $key => $value) {
|
|
|
|
|
if (is_string($value) && self::isIso8601Date($value)) {
|
|
|
|
|
$data[$key] = self::toDateFormat($value);
|
|
|
|
|
} elseif (is_array($value) || is_object($value)) {
|
|
|
|
|
$data[$key] = self::formatDates($value);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $data;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 단일 문자열이 ISO 8601 날짜인 경우
|
|
|
|
|
if (is_string($data) && self::isIso8601Date($data)) {
|
|
|
|
|
return self::toDateFormat($data);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $data;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* ISO 8601 날짜 형식인지 확인
|
|
|
|
|
*/
|
|
|
|
|
private static function isIso8601Date(string $value): bool
|
|
|
|
|
{
|
|
|
|
|
// 2025-12-22T15:00:00.000000Z 형식 매칭
|
|
|
|
|
return (bool) preg_match('/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z?$/', $value);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* ISO 8601 날짜를 Y-m-d 형식으로 변환
|
|
|
|
|
*/
|
|
|
|
|
private static function toDateFormat(string $value): string
|
|
|
|
|
{
|
|
|
|
|
try {
|
|
|
|
|
return \Carbon\Carbon::parse($value)->format('Y-m-d');
|
|
|
|
|
} catch (\Exception $e) {
|
|
|
|
|
return $value;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-06 17:45:49 +09:00
|
|
|
public function normalizeFiles(array $laravelFiles): array
|
|
|
|
|
{
|
2025-07-17 10:05:47 +09:00
|
|
|
$files = ['name' => [], 'type' => [], 'tmp_name' => [], 'size' => [], 'fileType' => []];
|
|
|
|
|
foreach ($laravelFiles as $file) {
|
|
|
|
|
$files['name'][] = $file->getClientOriginalName();
|
|
|
|
|
$files['type'][] = $file->getClientMimeType();
|
|
|
|
|
$files['tmp_name'][] = $file->getPathname();
|
|
|
|
|
$files['size'][] = $file->getSize();
|
|
|
|
|
$files['fileType'][] = '';
|
|
|
|
|
}
|
2025-11-06 17:45:49 +09:00
|
|
|
|
2025-07-17 10:05:47 +09:00
|
|
|
return $files;
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-06 17:45:49 +09:00
|
|
|
// DebugQuery Helper
|
2025-07-17 10:05:47 +09:00
|
|
|
public static function debugQueryLog(): array
|
|
|
|
|
{
|
|
|
|
|
$logs = DB::getQueryLog();
|
|
|
|
|
|
2025-08-14 17:20:28 +09:00
|
|
|
return collect($logs)
|
|
|
|
|
->skip(3)
|
|
|
|
|
->map(function ($log) {
|
|
|
|
|
$query = $log['query'];
|
|
|
|
|
foreach ($log['bindings'] as $binding) {
|
2025-11-06 17:45:49 +09:00
|
|
|
$binding = is_numeric($binding) ? $binding : "'".addslashes($binding)."'";
|
2025-08-14 17:20:28 +09:00
|
|
|
$query = preg_replace('/\\?/', $binding, $query, 1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// \n 제거
|
|
|
|
|
$query = str_replace(["\n", "\r"], ' ', $query)." (time: {$log['time']})";
|
2025-11-06 17:45:49 +09:00
|
|
|
|
2025-08-14 17:20:28 +09:00
|
|
|
return trim($query);
|
|
|
|
|
})->toArray();
|
2025-07-17 10:05:47 +09:00
|
|
|
}
|
|
|
|
|
|
2025-11-06 17:45:49 +09:00
|
|
|
// ApiResponse Helper
|
2025-07-17 10:05:47 +09:00
|
|
|
public static function success(
|
|
|
|
|
$data = null,
|
|
|
|
|
string $message = '요청 성공',
|
2025-12-08 19:03:50 +09:00
|
|
|
array $debug = [],
|
|
|
|
|
int $statusCode = 200
|
2025-07-17 10:05:47 +09:00
|
|
|
): JsonResponse {
|
2025-12-24 19:48:15 +09:00
|
|
|
// ISO 8601 날짜를 Y-m-d 형식으로 변환
|
|
|
|
|
$formattedData = self::formatDates($data);
|
|
|
|
|
|
2025-07-17 10:05:47 +09:00
|
|
|
$response = [
|
|
|
|
|
'success' => true,
|
|
|
|
|
'message' => $message,
|
2025-12-24 19:48:15 +09:00
|
|
|
'data' => $formattedData,
|
2025-07-17 10:05:47 +09:00
|
|
|
];
|
2025-11-06 17:45:49 +09:00
|
|
|
if (! empty($debug)) {
|
|
|
|
|
$response['query'] = $debug;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-08 19:03:50 +09:00
|
|
|
return response()->json($response, $statusCode);
|
2025-07-17 10:05:47 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static function error(
|
|
|
|
|
string $message = '요청 실패',
|
|
|
|
|
int $code = 400,
|
|
|
|
|
array $error = []
|
|
|
|
|
): JsonResponse {
|
|
|
|
|
return response()->json([
|
|
|
|
|
'success' => false,
|
2026-01-22 20:48:43 +09:00
|
|
|
'message' => "[{$code}] {$message}",
|
2025-07-17 10:05:47 +09:00
|
|
|
'error' => [
|
|
|
|
|
'code' => $code,
|
|
|
|
|
'details' => $error['details'] ?? null,
|
|
|
|
|
],
|
|
|
|
|
], $code);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static function validate(
|
|
|
|
|
bool $condition,
|
|
|
|
|
string $message = 'Validation failed',
|
|
|
|
|
int $code = 422,
|
|
|
|
|
array $extra = []
|
|
|
|
|
): ?JsonResponse {
|
|
|
|
|
return $condition ? null : self::error($message, $code, $extra);
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-26 14:25:45 +09:00
|
|
|
public static function response($type = '', $query = '', $key = ''): array
|
2025-07-17 10:05:47 +09:00
|
|
|
{
|
2025-08-14 17:20:28 +09:00
|
|
|
$debug = app()->environment('local') && request()->is('api/*');
|
2025-07-17 10:05:47 +09:00
|
|
|
|
|
|
|
|
$result = match ($type) {
|
2025-11-06 17:45:49 +09:00
|
|
|
'get' => $key ? $query->get()->keyBy($key) : $query->get(),
|
2025-07-17 10:05:47 +09:00
|
|
|
'getSub' => $query->get(),
|
|
|
|
|
'count' => $query->count(),
|
|
|
|
|
'first' => $query->first(),
|
|
|
|
|
'success' => 'Success',
|
|
|
|
|
'result' => $query,
|
|
|
|
|
default => null,
|
|
|
|
|
};
|
|
|
|
|
|
2025-11-06 17:45:49 +09:00
|
|
|
if ($type == 'getSub') {
|
2025-07-17 10:05:47 +09:00
|
|
|
$array = $result->map(function ($item) {
|
|
|
|
|
return (array) $item;
|
|
|
|
|
})->toArray();
|
|
|
|
|
foreach ($array as $row) {
|
|
|
|
|
$data[$row[$key]][] = $row;
|
|
|
|
|
}
|
|
|
|
|
$result = $data ?? [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$response['data'] = $result;
|
2025-08-14 17:20:28 +09:00
|
|
|
$response['query'] = $debug ? self::debugQueryLog() : [];
|
|
|
|
|
|
|
|
|
|
// 다음 요청에 로그가 섞이지 않도록 비워준다 (로컬에서만 의미있음)
|
|
|
|
|
if ($debug) {
|
|
|
|
|
DB::flushQueryLog();
|
|
|
|
|
}
|
2025-07-17 10:05:47 +09:00
|
|
|
|
|
|
|
|
return $response;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static function handle(
|
|
|
|
|
callable $callback,
|
2025-08-14 00:55:08 +09:00
|
|
|
string $responseTitle = '요청'
|
2025-11-06 17:45:49 +09:00
|
|
|
): JsonResponse {
|
2025-07-17 10:05:47 +09:00
|
|
|
try {
|
|
|
|
|
$result = $callback();
|
|
|
|
|
|
2025-08-19 12:41:17 +09:00
|
|
|
// 이미 JsonResponse면 그대로 반환
|
2025-07-17 10:05:47 +09:00
|
|
|
if ($result instanceof JsonResponse) {
|
|
|
|
|
return $result;
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-19 12:41:17 +09:00
|
|
|
// [신규] 서비스가 에러 ‘신호 배열’을 반환한 경우 감지
|
|
|
|
|
// 허용 포맷 예:
|
|
|
|
|
// ['error' => 'NO_TENANT', 'code' => 400]
|
|
|
|
|
// ['code' => 404, 'message' => '데이터 없음']
|
|
|
|
|
if (is_array($result) && (
|
2025-11-06 17:45:49 +09:00
|
|
|
array_key_exists('error', $result) ||
|
|
|
|
|
(array_key_exists('code', $result) && is_numeric($result['code']) && (int) $result['code'] >= 400)
|
|
|
|
|
)
|
2025-08-19 12:41:17 +09:00
|
|
|
) {
|
2025-11-06 17:45:49 +09:00
|
|
|
$code = (int) ($result['code'] ?? 400);
|
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개 기본 폴더 (생산관리, 품질관리, 회계, 인사, 일반)
- 에러 처리 및 로깅
- 회원가입 시 자동 실행
2025-11-10 19:08:56 +09:00
|
|
|
$message = (string) ($result['message'] ?? ($result['error'] ?? '서버 에러'));
|
2025-08-19 12:41:17 +09:00
|
|
|
$details = $result['details'] ?? null;
|
|
|
|
|
|
|
|
|
|
// 에러에도 쿼리 로그 포함되도록 error()가 처리하게 맡김
|
|
|
|
|
return self::error($message, $code, ['details' => $details]);
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-08 19:03:50 +09:00
|
|
|
// 표준 박스( ['data'=>..., 'query'=>..., 'statusCode'=>...] ) 하위호환
|
2025-08-19 12:41:17 +09:00
|
|
|
if (is_array($result) && array_key_exists('data', $result)) {
|
2025-11-06 17:45:49 +09:00
|
|
|
$data = $result['data'];
|
2025-08-19 12:41:17 +09:00
|
|
|
$debug = $result['query'] ?? [];
|
2025-12-08 19:03:50 +09:00
|
|
|
$statusCode = $result['statusCode'] ?? 200;
|
2025-08-19 12:41:17 +09:00
|
|
|
} else {
|
|
|
|
|
// 그냥 도메인 결과만 반환한 경우
|
2025-11-06 17:45:49 +09:00
|
|
|
$data = $result;
|
2025-08-19 12:41:17 +09:00
|
|
|
$debug = (app()->environment('local') && request()->is('api/*'))
|
|
|
|
|
? self::debugQueryLog()
|
|
|
|
|
: [];
|
2025-12-08 19:03:50 +09:00
|
|
|
$statusCode = 200;
|
2025-08-19 12:41:17 +09:00
|
|
|
}
|
|
|
|
|
|
2025-12-08 19:03:50 +09:00
|
|
|
return self::success($data, $responseTitle, $debug, $statusCode);
|
2025-08-19 12:41:17 +09:00
|
|
|
|
2025-07-17 10:05:47 +09:00
|
|
|
} catch (\Throwable $e) {
|
2026-02-07 03:27:07 +09:00
|
|
|
// 모든 예외를 로깅 (디버깅용)
|
|
|
|
|
Log::error('API Exception', [
|
|
|
|
|
'message' => $e->getMessage(),
|
|
|
|
|
'exception' => get_class($e),
|
|
|
|
|
'file' => $e->getFile(),
|
|
|
|
|
'line' => $e->getLine(),
|
|
|
|
|
'url' => request()->fullUrl(),
|
|
|
|
|
'method' => request()->method(),
|
|
|
|
|
]);
|
2025-08-20 20:23:01 +09:00
|
|
|
|
2026-01-06 19:12:16 +09:00
|
|
|
// ValidationException - 422 Unprocessable Entity
|
|
|
|
|
if ($e instanceof \Illuminate\Validation\ValidationException) {
|
|
|
|
|
return response()->json([
|
|
|
|
|
'success' => false,
|
|
|
|
|
'message' => __('error.validation_failed'),
|
|
|
|
|
'errors' => $e->errors(),
|
|
|
|
|
], 422);
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-11 19:01:07 +09:00
|
|
|
// 품목 코드 중복 예외 - duplicate_id, duplicate_code 포함
|
|
|
|
|
if ($e instanceof DuplicateCodeException) {
|
|
|
|
|
return response()->json([
|
|
|
|
|
'success' => false,
|
|
|
|
|
'message' => $e->getMessage(),
|
|
|
|
|
'error' => ['code' => 400],
|
|
|
|
|
'duplicate_id' => $e->getDuplicateId(),
|
|
|
|
|
'duplicate_code' => $e->getDuplicateCode(),
|
|
|
|
|
], 400);
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-20 20:23:01 +09:00
|
|
|
// HttpException 계열은 상태코드/메시지를 그대로 반영
|
|
|
|
|
if ($e instanceof HttpException) {
|
2025-12-18 15:42:46 +09:00
|
|
|
$statusCode = $e->getStatusCode();
|
|
|
|
|
// 4xx 클라이언트 에러에는 스택 트레이스 제외, 5xx 서버 에러에만 debug 모드에서 포함
|
|
|
|
|
$includeTrace = $statusCode >= 500 && config('app.debug');
|
|
|
|
|
|
2025-08-20 20:23:01 +09:00
|
|
|
return self::error(
|
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개 기본 폴더 (생산관리, 품질관리, 회계, 인사, 일반)
- 에러 처리 및 로깅
- 회원가입 시 자동 실행
2025-11-10 19:08:56 +09:00
|
|
|
$e->getMessage() ?: '서버 에러',
|
2025-12-18 15:42:46 +09:00
|
|
|
$statusCode,
|
|
|
|
|
['details' => $includeTrace ? $e->getTraceAsString() : null]
|
2025-08-20 20:23:01 +09:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-07 03:27:07 +09:00
|
|
|
// 일반 예외는 500으로 처리, debug 모드에서만 상세 정보 포함
|
|
|
|
|
$errorMessage = config('app.debug') ? $e->getMessage() : '서버 에러';
|
|
|
|
|
|
|
|
|
|
return self::error($errorMessage, 500, [
|
2025-12-18 15:42:46 +09:00
|
|
|
'details' => config('app.debug') ? $e->getTraceAsString() : null,
|
2026-02-07 03:27:07 +09:00
|
|
|
'exception' => config('app.debug') ? get_class($e) : null,
|
2025-07-17 10:05:47 +09:00
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|