## 역할
Next.js 15 App Router API 라우트. 레거시 Java Spring Controller(`*.do` 엔드포인트)를 Node.js로 마이그레이션한 65개 이상의 엔드포인트. PostgreSQL raw SQL로 데이터베이스 통신.

## 공통 패턴

### 인증 체크 (모든 엔드포인트 필수)
```typescript
const user = await getSession();
if (!user) return NextResponse.json({ success: false }, { status: 401 });
```

### HTTP 메서드
- **GET** — 단순 조회 (메뉴, 카운트 등)
- **POST** — 목록 조회 + 필터링 + 저장/수정 (기본값)
- `actionType` 필드로 INSERT/UPDATE 구분: `actionType === "regist"` → INSERT

### 동적 SQL 파라미터
```typescript
const conditions = ["1=1"];
const params: unknown[] = [];
let idx = 1;
if (body.field) {
  conditions.push(`TABLE.field = $${idx++}`);
  params.push(body.field);
}
// LIKE 검색: COLUMN LIKE '%' || $${idx++} || '%'
```

### 응답 형식
| 유형 | 형식 |
|------|------|
| 목록 조회 | `{ RESULTLIST: [], TOTAL_CNT: number }` |
| 단순 조회 | `{ success: true, data: rows }` |
| 저장/수정 | `{ success: true, objId? }` |
| 메뉴 | `{ RESULT: [] }` |
| 페이지 권한 | `{ CREATE_AUTH_CNT, READ_AUTH_CNT, ... }` |
| 오류 | `{ success: false, message }` + HTTP 400/401/500 |

## 연결 고리
- `@/lib/session` → `getSession()` (모든 엔드포인트)
- `@/lib/db` → `queryRows`, `queryOne`, `execute`
- `@/lib/utils` → `createObjectId()` (INSERT PK 생성), `checkNull`
- `@/lib/auth` → `verifyCredentials` (login 전용)
- `@/lib/encrypt` → `encrypt` (login 전용)

## 공통 조회 API (드롭다운/공용 데이터)

신규 메뉴에서 아래 엔드포인트 바로 사용 — 별도 매퍼 이식 불필요.

- `/api/common/code-list` — 공통코드(`comm_code`) parent_code_id 기준. body `{ codeId }` → `{ data: [{CODE_ID, CODE_NAME, OBJID}] }`. 원본 `common.getCodeselect` 대응
- `/api/common/supply-list` — 거래처/고객사(`supply_mng`). body `{ CHARGER_TYPE? }` → `{ RESULTLIST: [{OBJID, SUPPLY_NAME, SUPPLY_CODE, STATUS}] }`. 원본 `common.getsupplyselect`
- `/api/common/project-list` — 프로젝트(`project_mgmt`). body `{ customer_cd? }` → `{ RESULTLIST: [{OBJID, PROJECT_NO, CUSTOMER_PROJECT_NAME, LABEL}] }`. 원본 `common.getProjectNameList`
- `/api/common/unit-list` — WBS 유닛(`pms_wbs_task`). body `{ contract_objid }` → `{ RESULTLIST: [{OBJID, UNIT_NO, TASK_NAME, UNIT_NAME}] }`. 원본 `common.getBomCodeList`
- `/api/common/product-list` — 양산제품(`product_mgmt`). → `{ RESULTLIST: [{OBJID, PRODUCT_CODE, NAME}] }`. 원본 `common.getProductCodeselect`
- `/api/common/upg-list` — 제품 UPG(`product_mgmt_upg_detail`). body `{ PRODUCT_MGMT_OBJID }` → `{ RESULTLIST: [{CODE, NAME}] }`. 원본 `common.getProductUPGNEWselect`
- `/api/admin/users` — 사용자 목록. → `{ RESULTLIST: [{USER_ID, USER_NAME, ...}] }`. 원본 `common.getUserselect`
- `/api/common/files` — 첨부파일 조회. body `{ objId }` → `{ success, files: [{OBJID, REAL_FILE_NAME, DOC_TYPE, ...}] }`
- `/api/common/file-upload` — 멀티파트 업로드. formData `{ file, targetObjId, docType, docTypeName }` → `{ success }`

프론트엔드에서 `SearchableSelect` 옵션으로 변환 패턴:

```tsx
fetch("/api/common/supply-list", { method: "POST", ... })
  .then(r => r.json())
  .then(j => setOptions((j.RESULTLIST || []).map(r => ({
    value: String(r.OBJID), label: String(r.SUPPLY_NAME),
  }))));
```

## 등록 절차
새 API 추가 시 파일만 생성하면 자동 라우팅:
```
src/app/api/{module}/{feature}/route.ts
```
별도 등록 파일 없음 (Next.js App Router 자동 인식).

## 숨겨진 스펙
- SQL alias 대문자: `SELECT id AS "ID"` (큰따옴표 필수, 없으면 PostgreSQL이 소문자 반환)
- 삭제 플래그: `COALESCE(IS_DEL, 'N') != 'Y'`
- 날짜 포맷: `TO_CHAR(regdate, 'YYYY-MM-DD')`
- 기본 정렬: `ORDER BY regdate DESC`
- 파일 저장 경로: `{FILE_STORAGE_PATH}/{YYYYMMDD}/{createObjectId()}{ext}`
- 파일 DB 테이블: `ATTACH_FILE_INFO` (OBJID, TARGET_OBJID, DOC_TYPE, FILE_PATH, WRITER)
- objId 타입: DB에 numeric/bigint, 조회 시 `objid::text AS "OBJID"` 문자열 변환
- 목록 LIMIT: 일반 없음(rows.length), 대시보드 300, 공통코드 500
- 관리자 권한 신속 처리: `user.isAdmin` → 모든 권한 카운트 = 1

@MISTAKES.md
