ERP-node/docs/image-file-storage-guide.md

8.1 KiB

이미지/파일 저장 방식 가이드

개요

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 값을 문자열로 저장합니다.

-- 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)를 초과하는 큰 숫자이므로 정밀도 손실이 발생합니다. 반드시 문자열로 전달해야 합니다.

// 잘못된 방법
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)을 읽어 직접 조회
-- 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을 변환하여 <img> 태그로 직접 표시합니다. 파일 관리(목록, 삭제 등) 기능은 없습니다.


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.objidnumeric 타입으로 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곳 수정.