# 이미지/파일 저장 방식 가이드 ## 개요 WACE 솔루션에서 이미지 및 파일은 **attach_file_info 테이블**에 메타데이터를 저장하고, 실제 파일은 **서버 디스크**에 저장하는 이중 구조를 사용합니다. --- ## 1. 데이터 흐름 ``` [사용자 업로드] → [백엔드 API] → [디스크 저장] + [DB 메타데이터 저장] ↓ ↓ /uploads/COMPANY_7/ attach_file_info 테이블 2026/02/06/ (objid, file_path, ...) 1770346704685_5.png ``` ### 저장 과정 1. 사용자가 파일 업로드 → `POST /api/files/upload` 2. 백엔드가 파일을 디스크에 저장: `/uploads/{company_code}/{YYYY}/{MM}/{DD}/{timestamp}_{filename}` 3. `attach_file_info` 테이블에 메타데이터 INSERT (objid, file_path, target_objid 등) 4. 비즈니스 테이블의 이미지 컬럼에 **파일 objid** 저장 (예: `item_info.image = '433765011963536400'`) ### 조회 과정 1. 비즈니스 테이블에서 이미지 컬럼 값(objid) 로드 2. `GET /api/files/preview/{objid}` 로 이미지 프리뷰 요청 3. 백엔드가 `attach_file_info`에서 objid로 파일 정보 조회 4. 디스크에서 실제 파일을 읽어 응답 --- ## 2. 테이블 구조 ### attach_file_info (파일 메타데이터) | 컬럼 | 타입 | 설명 | |------|------|------| | objid | numeric | 파일 고유 ID (PK, 큰 숫자) | | real_file_name | varchar | 원본 파일명 | | saved_file_name | varchar | 저장된 파일명 (timestamp_원본명) | | file_path | varchar | 저장 경로 (/uploads/COMPANY_7/2026/02/06/...) | | file_ext | varchar | 파일 확장자 | | file_size | numeric | 파일 크기 (bytes) | | target_objid | varchar | 연결 대상 (아래 패턴 참조) | | company_code | varchar | 회사 코드 (멀티테넌시) | | status | varchar | 상태 (ACTIVE, DELETED) | | writer | varchar | 업로더 ID | | regdate | timestamp | 등록일시 | | is_representative | boolean | 대표 이미지 여부 | ### 비즈니스 테이블 (예: item_info, company_mng) 이미지 컬럼에 `attach_file_info.objid` 값을 문자열로 저장합니다. ```sql -- item_info.image = '433765011963536400' -- company_mng.company_image = '413276787660035200' ``` --- ## 3. target_objid 패턴 `attach_file_info.target_objid`는 파일이 어디에 연결되어 있는지를 나타냅니다. | 패턴 | 예시 | 설명 | |------|------|------| | 템플릿 모드 | `screen_files:140:comp_z4yffowb:image` | 화면 설계 시 업로드 (screenId:componentId:columnName) | | 레코드 모드 | `item_info:uuid-xxx:image` | 특정 레코드에 연결 (tableName:recordId:columnName) | --- ## 4. 파일 조회 API ### GET /api/files/preview/{objid} 이미지 프리뷰 (공개 접근 허용). ``` GET /api/files/preview/433765011963536400 → 200 OK (이미지 바이너리) ``` **주의: objid를 parseInt()로 변환하면 안 됩니다.** JavaScript의 `Number.MAX_SAFE_INTEGER`(9007199254740991)를 초과하는 큰 숫자이므로 **정밀도 손실**이 발생합니다. 반드시 **문자열**로 전달해야 합니다. ```typescript // 잘못된 방법 const fileRecord = await query("SELECT * FROM attach_file_info WHERE objid = $1", [parseInt(objid)]); // → parseInt("433765011963536400") = 433765011963536416 (16 차이!) // → DB에서 찾을 수 없음 → 404 // 올바른 방법 const fileRecord = await query("SELECT * FROM attach_file_info WHERE objid = $1", [objid]); // → PostgreSQL이 문자열 → numeric 자동 캐스팅 ``` ### GET /api/files/component-files 컴포넌트별 파일 목록 조회 (인증 필요). ``` GET /api/files/component-files?screenId=149&componentId=comp_z4yffowb&tableName=item_info&recordId=uuid-xxx&columnName=image ``` **조회 우선순위:** 1. **데이터 파일**: `target_objid = '{tableName}:{recordId}:{columnName}'` 패턴으로 조회 2. **템플릿 파일**: `target_objid = 'screen_files:{screenId}:{componentId}:{columnName}'` 패턴으로 조회 3. **레코드 컬럼 값 조회 (fallback)**: 위 두 방법으로 파일을 찾지 못하면, 비즈니스 테이블의 레코드에서 해당 컬럼 값(파일 objid)을 읽어 직접 조회 ```sql -- fallback: 레코드의 image 컬럼에 저장된 objid로 직접 조회 SELECT "image" FROM "item_info" WHERE id = $1; -- → '433765011963536400' SELECT * FROM attach_file_info WHERE objid = '433765011963536400' AND status = 'ACTIVE'; ``` --- ## 5. 프론트엔드 컴포넌트 ### v2-file-upload (FileUploadComponent.tsx) 현재 사용되는 V2 파일 업로드 컴포넌트입니다. **파일 경로**: `frontend/lib/registry/components/v2-file-upload/FileUploadComponent.tsx` #### 이미지 로드 방식 1. **formData의 컬럼 값으로 로드**: `formData[columnName]`에 파일 objid가 있으면 `/api/files/preview/{objid}`로 이미지 표시 2. **getComponentFiles API로 로드**: target_objid 패턴으로 서버에서 파일 목록 조회 #### 상태 관리 - `uploadedFiles` state: 현재 표시 중인 파일 목록 - `localStorage` 백업: `fileUpload_{componentId}_{columnName}` 키로 저장 - `window.globalFileState`: 전역 파일 상태 (컴포넌트 간 동기화) #### 등록/수정 모드 구분 - **수정 모드** (isRecordMode=true, recordId 있음): localStorage/서버에서 기존 파일 복원 - **등록 모드** (isRecordMode=false, recordId 없음): localStorage 복원 스킵, 빈 상태로 시작 - **단일 폼 화면** (회사정보 등): `formData[columnName]`의 objid 값으로 이미지 자동 로드 ### file-upload (레거시) **파일 경로**: `frontend/lib/registry/components/file-upload/FileUploadComponent.tsx` V2MediaRenderer에서 사용하는 레거시 컴포넌트. v2-file-upload와 유사하지만 별도 파일입니다. ### ImageWidget **파일 경로**: `frontend/components/screen/widgets/types/ImageWidget.tsx` 단순 이미지 표시용 위젯. 파일 업로드 기능은 있으나, `getFullImageUrl()`로 URL을 변환하여 `` 태그로 직접 표시합니다. 파일 관리(목록, 삭제 등) 기능은 없습니다. --- ## 6. 디스크 저장 구조 ``` backend-node/uploads/ ├── COMPANY_7/ # 회사별 격리 │ ├── 2026/ │ │ ├── 01/ │ │ │ └── 08/ │ │ │ └── 1767863580718_img.jpg │ │ └── 02/ │ │ └── 06/ │ │ ├── 1770346704685_5.png │ │ └── 1770352493105_5.png ├── COMPANY_9/ │ └── ... └── company_*/ # 최고 관리자 전용 └── ... ``` --- ## 7. 수정 이력 (2026-02-06) ### parseInt 정밀도 손실 수정 **파일**: `backend-node/src/controllers/fileController.ts` `attach_file_info.objid`는 `numeric` 타입으로 `433765011963536400` 같은 매우 큰 숫자입니다. JavaScript의 `parseInt()`는 `Number.MAX_SAFE_INTEGER`(약 9 * 10^15)를 초과하면 정밀도 손실이 발생합니다. | objid (원본) | parseInt 결과 | 차이 | |:---|:---|:---:| | 396361999644927100 | 396361999644927104 | -4 | | 433765011963536400 | 433765011963536384 | +16 | | 1128460590844245000 | 1128460590844244992 | +8 | **수정**: `parseInt(objid)` → `objid` (문자열 직접 전달, 8곳) ### getComponentFiles fallback 추가 **파일**: `backend-node/src/controllers/fileController.ts` 수정 모달에서 이미지가 안 보이는 문제. `target_objid` 패턴이 일치하지 않을 때, 비즈니스 테이블의 레코드 컬럼 값으로 파일을 직접 조회하는 fallback 로직 추가. ### v2-file-upload 등록 모드 파일 잔존 방지 **파일**: `frontend/lib/registry/components/v2-file-upload/FileUploadComponent.tsx` 연속 등록 시 이전 등록의 이미지가 남아있는 문제. `loadComponentFiles`와 fallback 로직에서 등록 모드(recordId 없음)일 때 파일 복원을 스킵하도록 수정. ### ORDER BY 기본 정렬 추가 **파일**: `backend-node/src/services/tableManagementService.ts` `sortBy` 파라미터가 없을 때 `ORDER BY created_date DESC`를 기본값으로 적용. 4곳 수정.