feat: 모모유통 유통관리 ERP (Next.js 16) — MOMO 브랜딩 + distribution DB + momo.junggomoa.com
- fito-nextjs 기반으로 재구성 - 로그인: MOMO 로고 + 모모유통 + 유통관리 ERP, 하단에 본사/지사 주소 표시 - 사이드바 상단: MOMO 아이콘 + 모모유통 + 유통관리 ERP - 파비콘: /src/app/icon.svg (MOMO 그린 배지) - layout.tsx title: 모모유통 | 유통관리 ERP - DB: 183.99.177.40:5432/distribution (fito 스키마 import 완료) - Traefik: Host(momo.junggomoa.com), 컨테이너 momo-erp
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
---
|
||||
globs: src/app/api/**/*.ts
|
||||
---
|
||||
|
||||
# API Route 코딩 규칙
|
||||
|
||||
## 인증 체크
|
||||
모든 핸들러 첫 줄에 세션 검증 필수:
|
||||
```typescript
|
||||
const user = await getSession();
|
||||
if (!user) return NextResponse.json({ success: false }, { status: 401 });
|
||||
```
|
||||
|
||||
## import 순서
|
||||
1. `next/server` (NextRequest, NextResponse)
|
||||
2. `@/lib/session` (getSession)
|
||||
3. `@/lib/db` (queryRows, queryOne, execute)
|
||||
4. `@/lib/utils` (createObjectId, checkNull 등)
|
||||
5. 기타 (`@/lib/auth`, `@/lib/encrypt` — login 전용)
|
||||
|
||||
## 동적 SQL 파라미터
|
||||
```typescript
|
||||
const conditions: string[] = ["1=1"];
|
||||
const params: unknown[] = [];
|
||||
let idx = 1;
|
||||
|
||||
if (body.field) {
|
||||
conditions.push(`TABLE.COLUMN = $${idx++}`);
|
||||
params.push(body.field);
|
||||
}
|
||||
// LIKE: conditions.push(`COLUMN LIKE '%' || $${idx++} || '%'`);
|
||||
|
||||
const sql = `SELECT ... WHERE ${conditions.join(" AND ")} ORDER BY regdate DESC`;
|
||||
const rows = await queryRows(sql, params);
|
||||
```
|
||||
|
||||
## SQL alias 대문자
|
||||
PostgreSQL은 따옴표 없는 alias를 소문자로 반환한다. 대문자 유지하려면 큰따옴표 필수:
|
||||
```sql
|
||||
SELECT id AS "OBJID", name AS "USER_NAME" -- O (대문자 유지)
|
||||
SELECT id AS OBJID -- X (소문자 objid로 반환)
|
||||
```
|
||||
|
||||
## 응답 형식
|
||||
- 목록 조회: `{ RESULTLIST: rows, TOTAL_CNT: rows.length }`
|
||||
- 단순 조회: `{ success: true, data: rows }`
|
||||
- 저장/수정: `{ success: true }` 또는 `{ success: true, objId: newId }`
|
||||
- 오류: `{ success: false, message: "..." }` + HTTP 400/401/500
|
||||
|
||||
## 에러 처리
|
||||
```typescript
|
||||
try {
|
||||
// 비즈니스 로직
|
||||
} catch (error) {
|
||||
console.error("설명:", error);
|
||||
return NextResponse.json({ success: false, message: "처리 중 오류가 발생했습니다." }, { status: 500 });
|
||||
}
|
||||
```
|
||||
|
||||
## INSERT/UPDATE 구분
|
||||
`actionType` 필드로 분기:
|
||||
```typescript
|
||||
const { actionType } = body;
|
||||
if (actionType === "regist") {
|
||||
const newId = createObjectId();
|
||||
await execute(`INSERT INTO ...`, [...]);
|
||||
} else {
|
||||
await execute(`UPDATE ... WHERE OBJID = $${idx}`, [...]);
|
||||
}
|
||||
```
|
||||
|
||||
## SQL 공통 패턴
|
||||
- 삭제 플래그: `COALESCE(IS_DEL, 'N') != 'Y'`
|
||||
- 날짜 포맷: `TO_CHAR(regdate, 'YYYY-MM-DD')`
|
||||
- 기본 정렬: `ORDER BY regdate DESC`
|
||||
- objId 타입 변환: `objid::text AS "OBJID"`
|
||||
@@ -0,0 +1,62 @@
|
||||
---
|
||||
globs: src/components/**/*.tsx
|
||||
---
|
||||
|
||||
# 컴포넌트 코딩 규칙
|
||||
|
||||
## 폼 컴포넌트 (ui/)
|
||||
- `forwardRef` 패턴 사용 (Button, Input, Select)
|
||||
- disabled 스타일: `disabled:opacity-50 disabled:cursor-not-allowed`
|
||||
- focus 스타일: `focus:outline-none focus:ring-1 focus:ring-primary/50`
|
||||
- `cn()` 유틸리티로 Tailwind 클래스 병합
|
||||
|
||||
## CodeSelect props
|
||||
```tsx
|
||||
<CodeSelect
|
||||
codeId="CODE_GROUP_ID" // 필수: 코드 그룹 ID
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
placeholder="선택" // 기본값
|
||||
/>
|
||||
```
|
||||
- API: `POST /api/common/code-list` (body: `{ codeId }`)
|
||||
- 응답 필드: `CODE_ID`, `CODE_NAME`
|
||||
|
||||
## FileUpload props
|
||||
```tsx
|
||||
<FileUpload
|
||||
targetObjId="대상ID" // 필수
|
||||
docType="문서타입코드" // 필수
|
||||
docTypeName="문서타입명" // 필수
|
||||
onUploadComplete={() => fetchFiles()}
|
||||
/>
|
||||
```
|
||||
- formData 키: `"file"`, `"targetObjId"`, `"docType"`, `"docTypeName"`
|
||||
|
||||
## DataGrid GridColumn 인터페이스
|
||||
```typescript
|
||||
{
|
||||
title: string;
|
||||
field: string; // RESULTLIST의 대문자 필드명
|
||||
width?: number | string;
|
||||
hozAlign?: "left" | "center" | "right";
|
||||
formatter?: "money" | ((cell: unknown, row: Record<string, unknown>) => string);
|
||||
cellClick?: (row: Record<string, unknown>) => void;
|
||||
visible?: boolean;
|
||||
}
|
||||
```
|
||||
- formatter `"money"` → `numberWithCommas()` 자동 적용
|
||||
- 데이터 소스: `data` prop (배열) 또는 `dataUrl` (POST)
|
||||
- 빈 데이터 메시지: "데이터가 없습니다."
|
||||
|
||||
## SearchForm 구조
|
||||
```tsx
|
||||
<SearchForm onSearch={fetchData}>
|
||||
<SearchField label="검색어">
|
||||
<Input name="keyword" />
|
||||
</SearchField>
|
||||
<SearchField label="분류">
|
||||
<CodeSelect codeId="CATEGORY" name="category" />
|
||||
</SearchField>
|
||||
</SearchForm>
|
||||
```
|
||||
@@ -0,0 +1,65 @@
|
||||
---
|
||||
globs: src/app/(main)/**/*.tsx, src/app/(auth)/**/*.tsx, src/app/admin-panel/**/*.tsx
|
||||
---
|
||||
|
||||
# 페이지 코딩 규칙
|
||||
|
||||
## 기본 구조
|
||||
모든 페이지는 `"use client"` 클라이언트 컴포넌트:
|
||||
```tsx
|
||||
"use client";
|
||||
|
||||
import { useState, useCallback } from "react";
|
||||
import { DataGrid, GridColumn } from "@/components/grid/data-grid";
|
||||
import { SearchForm, SearchField } from "@/components/layout/search-form";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { CodeSelect } from "@/components/ui/code-select";
|
||||
import Swal from "sweetalert2";
|
||||
```
|
||||
|
||||
## 페이지 레이아웃 순서
|
||||
1. 제목: `<h2 className="text-lg font-bold text-gray-800 mb-4">{제목}</h2>`
|
||||
2. 검색 폼: `<SearchForm>` + `<SearchField>`
|
||||
3. 버튼 영역: 조회/등록/삭제
|
||||
4. 데이터 그리드: `<DataGrid>` + `<Pagination>`
|
||||
|
||||
## 데이터 조회
|
||||
```tsx
|
||||
const [data, setData] = useState<Record<string, unknown>[]>([]);
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
const res = await fetch("/api/{domain}/{resource}", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ year, category_cd }),
|
||||
});
|
||||
if (res.ok) {
|
||||
const json = await res.json();
|
||||
setData(json.RESULTLIST || []);
|
||||
}
|
||||
}, [year, category_cd]);
|
||||
```
|
||||
|
||||
## 팝업
|
||||
```tsx
|
||||
const left = (window.screen.width - 1200) / 2;
|
||||
const top = (window.screen.height - 550) / 2;
|
||||
window.open(url, "formPopup", `width=1200,height=550,left=${left},top=${top}`);
|
||||
```
|
||||
|
||||
## 삭제 확인
|
||||
```tsx
|
||||
if (selectedRows.length === 0) {
|
||||
Swal.fire("알림", "삭제할 항목을 선택하세요.", "warning");
|
||||
return;
|
||||
}
|
||||
```
|
||||
|
||||
## DataGrid 높이
|
||||
페이지별 `calc(100vh - 350px)` ~ `calc(100vh - 400px)` 사용.
|
||||
|
||||
## 년도 필터 (자주 사용)
|
||||
```tsx
|
||||
Array.from({ length: 5 }, (_, i) => new Date().getFullYear() - i)
|
||||
```
|
||||
@@ -0,0 +1,29 @@
|
||||
---
|
||||
globs: **/*.ts, **/*.tsx
|
||||
---
|
||||
|
||||
# TypeScript 공통 규칙
|
||||
|
||||
## 경로 alias
|
||||
`@/*` → `./src/*` (tsconfig paths). 항상 `@/` 접두사 사용:
|
||||
```typescript
|
||||
import { queryRows } from "@/lib/db"; // O
|
||||
import { queryRows } from "../../lib/db"; // X
|
||||
```
|
||||
|
||||
## import 그룹 순서
|
||||
1. React/Next.js (`react`, `next/*`)
|
||||
2. 외부 라이브러리 (`sweetalert2`, `jose`, `lucide-react`)
|
||||
3. 내부 모듈 (`@/lib/*`, `@/store/*`, `@/types/*`, `@/components/*`)
|
||||
|
||||
## 데이터 타입
|
||||
- API 응답 데이터: `Record<string, unknown>[]`
|
||||
- 그리드 행 데이터: `Record<string, unknown>`
|
||||
- 필드명은 대문자: `row.OBJID`, `row.USER_NAME`
|
||||
- 중앙 타입은 `@/types/index.ts`에서 import
|
||||
|
||||
## null 처리
|
||||
`checkNull(value)` 유틸리티 사용. 문자열 `"null"`, `"undefined"`도 빈 문자열로 변환.
|
||||
|
||||
## strict mode
|
||||
tsconfig에 `strict: true` 설정됨. 암시적 any 금지.
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(where *)",
|
||||
"Bash(docker --version)",
|
||||
"Bash(docker-compose *)",
|
||||
"Bash(docker compose *)",
|
||||
"Bash(git push*)",
|
||||
"Bash(git push -u origin*)",
|
||||
"Bash(docker run *)",
|
||||
"Bash(docker exec *)",
|
||||
"Bash(docker cp *)",
|
||||
"Bash(ssh *)",
|
||||
"Bash(scp *)",
|
||||
"Bash(sshpass *)"
|
||||
]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user