diff --git a/.claude/rules/api-routes.md b/.claude/rules/api-routes.md new file mode 100644 index 0000000..bc0c2a2 --- /dev/null +++ b/.claude/rules/api-routes.md @@ -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"` diff --git a/.claude/rules/components.md b/.claude/rules/components.md new file mode 100644 index 0000000..b8a987c --- /dev/null +++ b/.claude/rules/components.md @@ -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 + setValue(e.target.value)} + placeholder="선택" // 기본값 +/> +``` +- API: `POST /api/common/code-list` (body: `{ codeId }`) +- 응답 필드: `CODE_ID`, `CODE_NAME` + +## FileUpload props +```tsx + 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); + cellClick?: (row: Record) => void; + visible?: boolean; +} +``` +- formatter `"money"` → `numberWithCommas()` 자동 적용 +- 데이터 소스: `data` prop (배열) 또는 `dataUrl` (POST) +- 빈 데이터 메시지: "데이터가 없습니다." + +## SearchForm 구조 +```tsx + + + + + + + + +``` diff --git a/.claude/rules/pages.md b/.claude/rules/pages.md new file mode 100644 index 0000000..3d61045 --- /dev/null +++ b/.claude/rules/pages.md @@ -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. 제목: `

{제목}

` +2. 검색 폼: `` + `` +3. 버튼 영역: 조회/등록/삭제 +4. 데이터 그리드: `` + `` + +## 데이터 조회 +```tsx +const [data, setData] = useState[]>([]); + +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) +``` diff --git a/.claude/rules/typescript.md b/.claude/rules/typescript.md new file mode 100644 index 0000000..9674d02 --- /dev/null +++ b/.claude/rules/typescript.md @@ -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[]` +- 그리드 행 데이터: `Record` +- 필드명은 대문자: `row.OBJID`, `row.USER_NAME` +- 중앙 타입은 `@/types/index.ts`에서 import + +## null 처리 +`checkNull(value)` 유틸리티 사용. 문자열 `"null"`, `"undefined"`도 빈 문자열로 변환. + +## strict mode +tsconfig에 `strict: true` 설정됨. 암시적 any 금지. diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..4aa68cd --- /dev/null +++ b/.claude/settings.json @@ -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 *)" + ] + } +} diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..5a2606d --- /dev/null +++ b/.dockerignore @@ -0,0 +1,19 @@ +node_modules +.next +.git +.gitignore +README.md +.env* +!.env.production.example +!.env.development.example +.vscode +.idea +.DS_Store +npm-debug.log* +*.log +coverage +.claude +.dockerignore +Dockerfile* +docker-compose*.yml +tsconfig.tsbuildinfo diff --git a/.env.momo.example b/.env.momo.example new file mode 100644 index 0000000..aa14326 --- /dev/null +++ b/.env.momo.example @@ -0,0 +1,19 @@ +# 모모유통(MOMO) 추가 환경변수 — .env / .env.production 에 함께 설정 +# 기존 FITO 변수에 아래 항목을 추가합니다. + +# ============ DB ============ +# 모모유통 테이블도 동일 DATABASE_URL 의 momo_* 테이블에 저장됩니다. +# DATABASE_URL 은 기존과 동일하게 사용 + +# ============ SMTP (메일 발송) ============ +# 발주 승인 시 거래명세표를 메일로 자동 발송합니다. +# 미설정 시: 메일은 jsonTransport 로 콘솔에만 출력 (개발 편의), DB mail_logs 에는 SENT 로 기록 +SMTP_HOST=smtp.daum.net +SMTP_PORT=465 +SMTP_USER=momo8443@daum.net +SMTP_PASS=__다음 메일 앱 비밀번호__ +SMTP_FROM=모모유통 + +# ============ 거래명세표에 표시될 공급자 정보 ============ +MOMO_BANK_ACCOUNT=기업은행 434-115361-01-016 +MOMO_PHONE=010-6624-5315 diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml new file mode 100644 index 0000000..11b4633 --- /dev/null +++ b/.gitea/workflows/deploy.yml @@ -0,0 +1,71 @@ +name: Deploy momo-erp to production + +on: + push: + branches: + - main + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup SSH + run: | + mkdir -p ~/.ssh + echo "${{ secrets.DEPLOY_SSH_KEY }}" > ~/.ssh/id_rsa + chmod 600 ~/.ssh/id_rsa + ssh-keyscan -H ${{ secrets.DEPLOY_HOST }} >> ~/.ssh/known_hosts 2>/dev/null + + - name: Deploy via SSH + env: + SSH_USER: ${{ secrets.DEPLOY_USER }} + SSH_HOST: ${{ secrets.DEPLOY_HOST }} + DATABASE_URL: ${{ secrets.DATABASE_URL }} + NEXTAUTH_URL: ${{ secrets.NEXTAUTH_URL }} + NEXTAUTH_SECRET: ${{ secrets.NEXTAUTH_SECRET }} + MASTER_PWD: ${{ secrets.MASTER_PWD }} + AES_KEY: ${{ secrets.AES_KEY }} + SMTP_HOST: ${{ secrets.SMTP_HOST }} + SMTP_PORT: ${{ secrets.SMTP_PORT }} + SMTP_USER: ${{ secrets.SMTP_USER }} + SMTP_PASS: ${{ secrets.SMTP_PASS }} + SMTP_FROM: ${{ secrets.SMTP_FROM }} + MOMO_BANK_ACCOUNT: ${{ secrets.MOMO_BANK_ACCOUNT }} + MOMO_PHONE: ${{ secrets.MOMO_PHONE }} + run: | + ssh -o StrictHostKeyChecking=no "$SSH_USER@$SSH_HOST" bash -s << 'REMOTE' + set -e + DEPLOY_DIR="$HOME/momo-erp/source" + mkdir -p "$HOME/momo-erp" + if [ -d "$DEPLOY_DIR/.git" ]; then + cd "$DEPLOY_DIR" && git fetch origin && git reset --hard origin/main + else + git clone https://git.junggomoa.com/chpark/distribution_erp.git "$DEPLOY_DIR" + cd "$DEPLOY_DIR" + fi + cat > .env.production < +# This is NOT the Next.js you know + +This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` before writing any code. Heed deprecation notices. + diff --git a/CICD_SETUP.md b/CICD_SETUP.md new file mode 100644 index 0000000..0da6f84 --- /dev/null +++ b/CICD_SETUP.md @@ -0,0 +1,48 @@ +# CI/CD 배포 가이드 + +## 개요 + +`.gitea/workflows/deploy.yml` 워크플로가 `main` 브랜치 푸시 시 자동으로 +배포 서버(183.99.177.40)에 SSH 접속 → `docker compose up -d --build` 실행합니다. + +## Gitea 시크릿 등록 + +Gitea 저장소 → **Settings → Actions → Secrets** 에 다음 시크릿을 추가하세요: + +| 시크릿 이름 | 값 (예시) | +|-------------|----------| +| `DEPLOY_HOST` | `183.99.177.40` | +| `DEPLOY_USER` | `chpark` | +| `DEPLOY_SSH_KEY` | SSH 개인키 전체 (BEGIN/END 포함) | +| `DATABASE_URL` | `postgresql://postgres:qlalfqjsgh11@183.99.177.40:5432/distribution` | +| `NEXTAUTH_URL` | `https://momo.junggomoa.com` | +| `NEXTAUTH_SECRET` | 임의의 32바이트 hex (현재 .env.production 값 재사용 가능) | +| `MASTER_PWD` | `qlalfqjsgh11` | +| `AES_KEY` | `ILJIAESSECRETKEY` | + +### SSH 키 생성 (최초 1회) + +로컬에서: +```bash +ssh-keygen -t ed25519 -C "gitea-deploy" -f ~/.ssh/momo_deploy -N "" +# 공개키를 배포 서버에 등록 +ssh-copy-id -i ~/.ssh/momo_deploy.pub chpark@183.99.177.40 +# 개인키를 Gitea Secret `DEPLOY_SSH_KEY` 에 붙여넣기 +cat ~/.ssh/momo_deploy +``` + +## Gitea Actions Runner + +워크플로가 실행되려면 Gitea Actions Runner가 등록돼 있어야 합니다. +`git.junggomoa.com` 인스턴스에 Runner가 이미 있으면 이 단계는 생략. +없으면 배포 서버나 별도 머신에 [act_runner](https://gitea.com/gitea/act_runner) 설치 필요. + +## 수동 배포 (CI/CD 우회) + +긴급 시: +```bash +ssh chpark@183.99.177.40 +cd ~/momo-erp/source +git pull +docker compose -f docker-compose.prod.yml up -d --build +``` diff --git a/CLAUDE.md b/CLAUDE.md index 82b0db0..6a2f73c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,136 +1,77 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## 프로젝트 개요 - -ILSHIN PLM (Product Lifecycle Management) 솔루션 - 제품 수명 주기 전체를 관리하는 Java 기반 엔터프라이즈 웹 애플리케이션 - -## 기술 스택 - -- **Java 7** (Eclipse 프로젝트, JRE 1.7) -- **Spring Framework 3.2.4** - MVC, IoC 컨테이너 -- **MyBatis 3.2.3** - SQL 매핑 프레임워크 -- **PostgreSQL** - 데이터베이스 -- **Apache Tomcat 7.0** - 웹/애플리케이션 서버 -- **JSP/jQuery/jqGrid** - 프론트엔드 -- **Apache Tiles 3.0.5** - 템플릿 엔진 - -## 프로젝트 구조 - -``` -plm-ilshin/ -├── src/ # Java 소스 코드 -│ └── com/pms/ -│ ├── controller/ # Spring MVC 컨트롤러 -│ ├── service/ # 비즈니스 로직 -│ ├── dao/ # 데이터 액세스 계층 -│ └── mapper/ # MyBatis XML 매퍼 파일 -├── WebContent/ -│ ├── WEB-INF/ -│ │ ├── classes/ # 컴파일된 클래스 (빌드 산출물) -│ │ ├── lib/ # JAR 라이브러리 -│ │ ├── view/ # JSP 뷰 파일 -│ │ ├── web.xml # 웹 애플리케이션 설정 -│ │ └── dispatcher-servlet.xml # Spring MVC 설정 -│ ├── resources/ # 정적 리소스 (CSS, JS, 이미지) -│ └── *.jsp # JSP 페이지 -└── docker-compose.*.yml # Docker 구성 파일 -``` - -## 빌드 및 실행 - -### Eclipse 빌드 -- Eclipse IDE에서 직접 빌드 (Maven/Gradle 미사용) -- 소스: `src/` → 컴파일 결과: `WebContent/WEB-INF/classes/` - -### Docker 환경 실행 - -```bash -# 개발 환경 실행 -docker-compose -f docker-compose.dev.yml up --build -d - -# 프로덕션 환경 실행 -docker-compose -f docker-compose.prod.yml up --build -d - -# 로그 확인 -docker-compose -f docker-compose.dev.yml logs -f - -# 컨테이너 중지 -docker-compose -f docker-compose.dev.yml down -``` - -### 접속 정보 -- 애플리케이션: `http://localhost:9090` (개발) -- DB 연결: PostgreSQL (JNDI: `java:comp/env/plm`) - -## 주요 모듈 - -### 핵심 기능 패키지 -- `com.pms.controller` - MVC 컨트롤러 -- `com.pms.ions.itemmgmt` - 품목 관리 (BOM, 자재 등) -- `com.pms.ions.productioninventory` - 생산/재고 관리 -- `com.pms.salesmgmt` - 영업 관리 - -### 주요 컨트롤러 -- `BomController` - BOM 관리 -- `ImItemController` - 품목 마스터 -- `ProductionInventoryController` - 생산 재고 -- `OrderMgmtController` - 주문 관리 -- `ECRController`, `EOController` - 설계 변경 관리 - -## 데이터베이스 - -### MyBatis 매퍼 위치 -- 소스: `src/com/pms/mapper/*.xml` -- 빌드: `WebContent/WEB-INF/classes/com/pms/mapper/*.xml` - -### 주요 매퍼 파일 -- `bom.xml` - BOM 쿼리 -- `imItem.xml` - 품목 관리 -- `productionplanning.xml` - 생산 계획 -- `orderMgmt.xml` - 주문 관리 - -## 개발 주의사항 - -1. **Java 버전**: Java 7 문법만 사용 (람다, 스트림 API 사용 불가) -2. **Spring 버전**: Spring 3.2.4 기능만 사용 -3. **문자 인코딩**: UTF-8 설정 필수 -4. **세션 관리**: Spring Security 미사용, 직접 구현된 세션 관리 사용 -5. **파일 구조**: Eclipse Dynamic Web Project 구조 유지 -6. **빌드**: IDE 기반 빌드, Maven/Gradle 미사용 - -## 테스트 - -### 단위 테스트 -```bash -# 테스트 프레임워크 미구성 - 수동 테스트 필요 -# Tomcat 서버에 배포 후 브라우저에서 기능 테스트 -``` - -### 로컬 테스트 -1. Eclipse에서 Tomcat 서버 추가 -2. 프로젝트를 서버에 배포 -3. `http://localhost:8080/ilshin` 접속 - -## 디버깅 - -### 로그 확인 -- Log4j 설정: `WebContent/WEB-INF/log4j.xml` -- 로그 레벨 조정 가능 - -### Docker 디버깅 -```bash -# 컨테이너 내부 접속 -docker exec -it plm-ilshin-dev bash - -# Tomcat 로그 확인 -docker logs plm-ilshin-dev -``` - -## 주요 설정 파일 - -- `WebContent/WEB-INF/web.xml` - 웹 애플리케이션 설정 -- `WebContent/WEB-INF/dispatcher-servlet.xml` - Spring MVC 설정 -- `SETTING_GUIDE.txt` - Tomcat Context 설정 (JNDI DataSource) -- `.classpath`, `.project` - Eclipse 프로젝트 설정 \ No newline at end of file +# FITO — (주)피토 PLM (Next.js 풀스택) + +기존 Java/Spring MVC + JSP + MyBatis 기반 FITO 시스템을 Next.js 15 + Node.js로 1:1 컨버전한 제조/PLM 시스템. + +> **프로젝트 히스토리**: `woosung-nextjs`에서 피벗. 이전 상태는 태그 `woosung-v1-snapshot`에 보존. 우성 도메인 코드(api/*, app/(main)/*)는 점진적으로 FITO로 교체 중. + +--- + +## 디렉토리별 CLAUDE.md + +| 디렉토리 | 내용 | +|---------|------| +| [src/lib/](src/lib/CLAUDE.md) | 핵심 인프라: DB, 인증, 세션, 암호화, 상수 | +| [src/store/](src/store/CLAUDE.md) | Zustand 전역 상태 (auth, menu, theme) | +| [src/types/](src/types/CLAUDE.md) | TypeScript 중앙 타입 정의 | +| [src/components/](src/components/CLAUDE.md) | UI/레이아웃/그리드 공통 컴포넌트 | +| [src/app/api/](src/app/api/CLAUDE.md) | API 라우트 | +| [src/app/(main)/](src/app/(main)/CLAUDE.md) | 업무 페이지 (인증 필수) | +| [src/app/(auth)/](src/app/(auth)/CLAUDE.md) | 로그인 페이지 | +| [src/app/admin-panel/](src/app/admin-panel/CLAUDE.md) | 관리자 패널 (팝업) | + +--- + +## 기술 스택 +- **Frontend**: Next.js 15 (App Router), React 19, TypeScript, Tailwind CSS +- **Backend**: Next.js API Routes (Node.js) +- **Database**: PostgreSQL (외부 공용 서버 `211.115.91.141:11140/fito`, raw SQL via `pg`) +- **인증**: JWT (jose) + Cookie 기반 세션 +- **상태관리**: Zustand +- **UI**: SweetAlert2, Lucide Icons, Custom DataGrid (TanStack React Table) + +## 원본 프로젝트 매핑 +원본 FITO 위치: `/Users/jhj/FITO/` (Java 7 + Spring 3.2.4 + MyBatis 3.2.3 + JSP + Tomcat 7) + +- `src/app/api/` → Java Controller (`*.do` 엔드포인트, `com.pms.controller.*`) +- `src/app/(main)/` → JSP 페이지 (`/WebContent/WEB-INF/view/`) +- `src/lib/db.ts` → MyBatis SqlSession (queryRows = selectList, queryOne = selectOne) +- `src/lib/auth.ts` → LoginService +- `src/lib/session.ts` → SessionManager + PersonBean +- `src/lib/encrypt.ts` → EncryptUtil (AES) +- `src/lib/constants.ts` → Constants.java +- `src/lib/utils.ts` → CommonUtils.java + +## DB 쿼리 패턴 +기존 MyBatis XML mapper의 SQL을 `queryRows`/`queryOne`/`execute`로 직접 실행. +파라미터는 `$1`, `$2` 형태의 PostgreSQL prepared statement 사용. + +**FITO mapper 위치**: `/Users/jhj/FITO/src/com/pms/mapper/` (77개 XML) + +## 새 모듈 추가 시 +1. `src/app/api/[module]/route.ts` — API 라우트 (기존 Controller 대응) +2. `src/app/(main)/[module]/page.tsx` — 페이지 (기존 JSP 대응) +3. FITO MyBatis XML에서 SQL 복사하여 raw query로 변환 +4. `SearchForm` + `DataGrid` 컴포넌트 조합으로 목록 페이지 구성 + +## 미들웨어 (인증) +`src/middleware.ts`가 모든 요청을 가로채서 `plm-session` 쿠키 존재 여부 확인. +- 공개 경로: `/login`, `/api/auth/login`, `/_next`, `/favicon.ico` +- 세션 없는 일반 요청 → `/login`으로 리다이렉트 +- 세션 없는 API 요청 → 401 JSON 응답 + +## 실행 +```bash +npm run dev # 개발 서버 (localhost:3000) +``` + +## 환경변수 + +- `.env.development` — 로컬 개발용 (외부 FITO DB 접속) +- `.env.development.example` — 팀 공유 템플릿 (비밀번호 마스킹) +- `.env` — Prisma CLI용 기본값 + +## 배포 표준 (예정) + +- Docker Compose dev/prod 분리 +- Traefik 리버스 프록시 + `fito.wace.me` 서브도메인 +- DB는 외부 `211.115.91.141:11140/fito` 공유 (컨테이너 내부 DB 없음) diff --git a/Dockerfile b/Dockerfile index 296a071..218628d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,71 +1,44 @@ -# Multi-stage build for production -# Stage 1: Build stage - compile Java sources -FROM dockerhub.wace.me/tomcat:7.0.94-jre7-alpine.linux AS builder +# Multi-stage build for FITO Next.js 풀스택 — production +# Stage 1: 의존성 설치 +FROM node:20-alpine AS deps +WORKDIR /app +COPY package.json package-lock.json ./ +RUN npm ci -# Install JDK for compilation (JRE image doesn't have javac) -RUN apk add --no-cache openjdk7 +# Stage 2: 빌드 +FROM node:20-alpine AS builder +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY . . -# Set working directory -WORKDIR /build +# 환경변수 (빌드 타임에 주입되는 NEXT_PUBLIC_* 변수) +ENV NEXT_TELEMETRY_DISABLED=1 +ENV NODE_ENV=production -# Copy source code and libraries -COPY src ./src -COPY WebContent ./WebContent +RUN npm run build -# Create classes directory -RUN mkdir -p WebContent/WEB-INF/classes +# Stage 3: 런타임 (최소 이미지) +FROM node:20-alpine AS runner +WORKDIR /app -# Compile Java sources (include Tomcat servlet API in classpath) -RUN find src -name "*.java" -print0 | xargs -0 javac \ - -encoding UTF-8 \ - -source 1.7 \ - -target 1.7 \ - -d WebContent/WEB-INF/classes \ - -cp "WebContent/WEB-INF/lib/*:/usr/local/tomcat/lib/*" \ - -Xlint:-options \ - -Xlint:-deprecation \ - -Xlint:-unchecked +ENV NODE_ENV=production +ENV NEXT_TELEMETRY_DISABLED=1 +ENV PORT=3000 +ENV HOSTNAME=0.0.0.0 -# Copy resources (XML, properties files) -RUN find src -type f \( -name "*.xml" -o -name "*.properties" \) | while read -r filepath; do \ - relative_path="${filepath#src/}"; \ - target_file="WebContent/WEB-INF/classes/$relative_path"; \ - mkdir -p "$(dirname "$target_file")"; \ - cp "$filepath" "$target_file"; \ - done +# 비루트 사용자 (보안) +RUN addgroup --system --gid 1001 nodejs && \ + adduser --system --uid 1001 nextjs -# Verify compilation -RUN CLASS_COUNT=$(find WebContent/WEB-INF/classes -name "*.class" | wc -l); \ - if [ $CLASS_COUNT -eq 0 ]; then \ - echo "ERROR: No Java classes were compiled!"; \ - exit 1; \ - else \ - echo "Successfully compiled $CLASS_COUNT Java classes"; \ - fi +# standalone 번들 복사 +COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ +COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static +COPY --from=builder --chown=nextjs:nodejs /app/public ./public -# Stage 2: Runtime stage -FROM dockerhub.wace.me/tomcat:7.0.94-jre7-alpine.linux AS production +# 파일 업로드 디렉토리 +RUN mkdir -p /data_storage && chown nextjs:nodejs /data_storage -# Install fonts for POI Excel generation -RUN apk add --no-cache fontconfig ttf-dejavu +USER nextjs +EXPOSE 3000 -# Remove default webapps -RUN rm -rf /usr/local/tomcat/webapps/* - -# Copy compiled application from builder stage -COPY --from=builder /build/WebContent /usr/local/tomcat/webapps/ROOT - -# Copy source for reference (optional) -COPY src /usr/local/tomcat/webapps/ROOT/WEB-INF/src - -# Copy custom Tomcat context configuration for JNDI -COPY ./tomcat-conf/context.xml /usr/local/tomcat/conf/context.xml - -# Configure Tomcat Connector for UTF-8 URI encoding -RUN sed -i 's/- - -DDB_URL=jdbc:postgresql://${DB_HOST}:${DB_PORT}/${DB_NAME} - -DDB_USERNAME=${DB_USERNAME} - -DDB_PASSWORD=${DB_PASSWORD} + - .env.development volumes: - - momo-erp-project_data:/data_storage - - momo-erp-app_data:/path/inside/container + - ./src:/app/src + - ./public:/app/public + - ./prisma:/app/prisma + - ./next.config.ts:/app/next.config.ts + - ./tsconfig.json:/app/tsconfig.json + - ./package.json:/app/package.json + - plm-fito-next-data:/data_storage restart: unless-stopped volumes: - momo-erp-project_data: - momo-erp-app_data: + plm-fito-next-data: diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index b36a2d9..f27bc06 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -1,34 +1,29 @@ +# 운영 배포 (Traefik + momo.junggomoa.com) +# 대상 서버: 183.99.177.40 (Traefik v2.11 외부 네트워크 traefik-net 사용) +# 사용: docker compose -f docker-compose.prod.yml up -d --build services: momo-erp: build: context: . dockerfile: Dockerfile + image: momo-erp:latest container_name: momo-erp restart: always env_file: - .env.production - environment: - CATALINA_OPTS: >- - -DDB_URL=jdbc:postgresql://${DB_HOST}:${DB_PORT}/${DB_NAME} - -DDB_USERNAME=${DB_USERNAME} - -DDB_PASSWORD=${DB_PASSWORD} - -Xms512m - -Xmx1536m - -XX:MaxPermSize=256m - -Djava.net.preferIPv4Stack=true - -Dfile.encoding=UTF-8 volumes: - - /home/momo-erp/project_data:/data_storage - - /home/momo-erp/app_data:/path/inside/container + - ./data_storage:/data_storage + networks: + - traefik-net labels: - traefik.enable=true + - traefik.docker.network=traefik-net - traefik.http.routers.momo-erp.rule=Host(`momo.junggomoa.com`) - traefik.http.routers.momo-erp.entrypoints=websecure,web - traefik.http.routers.momo-erp.tls=true - traefik.http.routers.momo-erp.tls.certresolver=le - - traefik.http.services.momo-erp.loadbalancer.server.port=8080 + - traefik.http.services.momo-erp.loadbalancer.server.port=3000 networks: - default: - external: - name: toktork_server_default + traefik-net: + external: true diff --git a/docs/2026_0410_ 금_라인업.xlsx b/docs/2026_0410_ 금_라인업.xlsx new file mode 100644 index 0000000..f6021a2 Binary files /dev/null and b/docs/2026_0410_ 금_라인업.xlsx differ diff --git a/docs/MOMO_DISTRIBUTION_SPEC.md b/docs/MOMO_DISTRIBUTION_SPEC.md new file mode 100644 index 0000000..932cd40 --- /dev/null +++ b/docs/MOMO_DISTRIBUTION_SPEC.md @@ -0,0 +1,798 @@ +# 모모유통 — 도매 유통 관리 시스템 개발 스펙 + +> **버전**: 0.1 (초안) +> **작성일**: 2026-04-25 +> **대상 도메인**: `momo.junggomoa.com` +> **기술 스택**: Next.js 15 (App Router) · React 19 · TypeScript · Tailwind · PostgreSQL (raw SQL via `pg`) · JWT 세션 · Zustand · TanStack Table · SweetAlert2 + +--- + +## 0. 요약 (TL;DR) + +모모유통은 **대형 도매 유통 업체**다. 본사는 도매처에서 물품을 사들여 **자체 창고에 적재**한 뒤, 가입된 **소매 대리점(소상공인)** 들이 시스템에서 출고를 요청하면 담당자가 검수·승인하여 출고한다. + +- **사용자 그룹 2종** + - **일반 사용자(대리점)** — 가입·로그인 후 재고 보유 품목을 보고 **발주(출고요청)** 작성 + - **관리자(모모유통 담당자)** — 품목·재고·창고 마스터 관리, 발주서 승인, 거래명세서/계산서 발행, 통계 조회 +- **핵심 워크플로우**: `발주요청` → (담당자 승인) → `발주완료` + **메일 자동 발송** → (월말 일괄) → `계산서 발행 완료` +- **면세/과세 구분**: 품목 코드 접두어 `M` = 면세 (예: `M유정란`, `M꽃계탕`). 거래명세서·매출통계에서 `면세매출합` / `과세매출합` 분리 집계. +- **이메일 발송**: 발주 승인 시 가입 시 등록한 이메일로 **거래명세표(첨부 또는 본문)** 발송. + +--- + +## 1. 사용자 역할 / 권한 + +| 역할 | 코드 | 설명 | 접근 가능 메뉴 | +|---|---|---|---| +| 일반 사용자 | `USER` | 대리점/소매상 | 대시보드(본인용), 재고 조회(보유 수량 노출), **출고요청서 작성**, 본인 발주 이력 조회, 본인 미수금/계산서 조회 | +| 관리자 | `ADMIN` | 모모유통 담당자 | 전체 메뉴 (품목·재고·창고·발주서 승인·거래명세서·계산서·통계·회원관리) | + +> `users` 테이블의 `role` 컬럼으로 구분. 가입 직후 기본값은 `USER`. 관리자 승격은 어드민 패널에서 수동. + +--- + +## 2. 회원가입 / 로그인 + +### 2.1 가입 화면 (`/signup`) +- 필드 + - **이메일** (필수, 유니크, 로그인 ID 겸용) + - **사용자명 (업체명)** (필수, 표시명) + - **비밀번호** / **비밀번호 확인** (필수) + - **연락처** (선택) + - **사업자등록번호** (선택, 거래명세서 출력용) + - **대표자명** (선택, 거래명세서 출력용) +- 검증 + - 이메일 형식, 비밀번호 8자 이상, 업체명 중복은 허용 (이메일만 유니크) +- 저장 시 + - `password`는 bcrypt 해시 (`bcryptjs` 권장, `pg` 환경에서 트라이비얼) + - `role = 'USER'`, `status = 'ACTIVE'`, `is_del = 'N'` + +### 2.2 로그인 화면 (`/login`) — 기존 화면 재사용 +- 입력: 이메일 + 비밀번호 +- 성공 시: JWT 발급 → `plm-session` 쿠키 설정 → `/` 리다이렉트 +- 가입 링크 추가: 로그인 폼 하단에 "회원가입" 버튼 + +### 2.3 미들웨어 정책 변경 +`src/middleware.ts`의 공개 경로에 `/signup`, `/api/auth/signup` 추가. + +--- + +## 3. 메뉴 정리 (기존 FITO/PLM → 모모유통) + +기존 PLM 메뉴 중 **불필요한 항목 삭제**, **유사 메뉴 재활용**, 신규 메뉴 추가. + +### 3.1 제거 (사용 안 함) +다음 디렉토리/메뉴는 폐기 — `src/app/(main)/` 및 `src/app/api/` 양쪽에서 정리 필요: + +- `bom`, `product/bom-list`, `product/bom-register` — BOM 관리 (제조 PLM 전용) +- `product/design-change`, `product/part-change` — 설계변경/부품변경 +- `product/spec`, `product/part-list`, `product/part-register` — 부품 마스터 +- `part`, `part-mgmt` — 부품 관리 +- `procurement-std` — 조달 표준 +- `production` — 생산 관리 +- `quality` — 품질 관리 +- `project` — 프로젝트 관리 +- `scm` — SCM +- `work` — 업무 (워크플로우) +- `cost`, `cost-mgmt` — 원가 관리 (※ 어드민용 매출/원가/마진 통계는 §10에서 별도 신규) +- `cs` — 고객지원 (필요 시 후속 단계로 보류) +- `fund` — 자금 (필요 시 후속 단계로 보류) +- `delivery` — 납품 (출고관리로 대체) +- `approval` — 결재 (단순 상태 전이로 대체) +- `purchase` — 매입 (`procurement` 신규로 대체) +- `sales` — 매출 (`statistics` 신규로 대체) + +### 3.2 재활용 (이름·로직 일부 변경) +| 기존 | 신규 | 비고 | +|---|---|---| +| `product/*` | `item/*` (품목 마스터) | 품목명/제조사/사진/면세여부/속성 | +| `inventory/list` | `inventory/list` | 창고별 현재고 — 그대로 | +| `inventory/status` | `inventory/status` | 입출고 이력 — 그대로 | +| `inventory/request` | (삭제) | 신규 `order` 메뉴로 대체 | +| `purchase-order` | `procurement` | 도매처 → 모모유통 매입 발주 (관리자 전용) | +| `order` | `order` | 대리점 → 모모유통 출고요청서 (핵심) | +| `dashboard` | `dashboard` | 그대로 — 콘텐츠 교체 | +| `admin` / `admin-panel` | `admin` | 회원/코드/메뉴 관리 | + +### 3.3 최종 메뉴 트리 + +``` +홈 (/) +├─ 대시보드 (/dashboard) +│ ├─ 일반: 내 발주 진행 현황, 추천/신규 품목, 미수금 +│ └─ 관리자: 발주 승인 대기, 재고 알림, 매출 그래프, 미수금 합계 +├─ 품목 관리 (/item) [관리자] +│ ├─ 품목 목록 (/item/list) +│ ├─ 품목 등록·수정 (/item/form) +│ └─ 제조사 관리 (/item/maker) +├─ 창고 관리 (/warehouse) [관리자] +│ ├─ 창고 목록 (/warehouse/list) +│ └─ 창고별 재고 (/warehouse/stock) +├─ 재고 관리 (/inventory) [공용 — 일반은 조회만] +│ ├─ 현재고 (/inventory/list) +│ ├─ 입출고 이력 (/inventory/history) +│ └─ 입고 등록 (/inventory/inbound) [관리자] +├─ 발주 관리 (/order) [공용] +│ ├─ 발주서 목록 (/order/list) (일반: 본인 / 관리자: 전체) +│ ├─ 발주서 작성 (/order/form) +│ └─ 거래명세표 (/order/statement/[id]) +├─ 매입(조달) 관리 (/procurement) [관리자] +│ ├─ 매입 발주서 (/procurement/list) +│ └─ 매입처 관리 (/procurement/vendor) +├─ 정산 관리 (/settlement) [관리자] +│ ├─ 거래명세서 일괄 발행 (/settlement/statement) +│ ├─ 계산서 발행 (/settlement/invoice) +│ └─ 입금 관리 (/settlement/payment) +├─ 통계 (/statistics) [관리자] +│ ├─ 일자별 발주 현황 (/statistics/daily) +│ ├─ 월간 누적 (/statistics/monthly) +│ ├─ 업체별 매출 (/statistics/by-company) +│ └─ 품목별 발주 (/statistics/by-item) +└─ 시스템 (/admin) [관리자] + ├─ 회원 관리 (/admin/users) + ├─ 공통 코드 (/admin/code) + └─ 메뉴 관리 (/admin/menu) +``` + +--- + +## 4. 데이터 모델 (PostgreSQL) + +> **명명 규칙**: 테이블·컬럼은 모두 `snake_case`. PK는 `objid TEXT` (기존 FITO 규약 — `createObjectId()` 사용). 삭제는 soft delete (`is_del CHAR(1) DEFAULT 'N'`). 생성/수정 시각은 `regdate`, `regid`, `update_date`, `update_id`. + +### 4.1 사용자 / 인증 + +```sql +CREATE TABLE users ( + objid TEXT PRIMARY KEY, + email VARCHAR(200) NOT NULL UNIQUE, + password_hash VARCHAR(200) NOT NULL, + company_name VARCHAR(200) NOT NULL, -- 업체명 (표시명) + ceo_name VARCHAR(100), -- 대표자명 + biz_no VARCHAR(20), -- 사업자등록번호 + phone VARCHAR(50), + role VARCHAR(20) NOT NULL DEFAULT 'USER', -- USER | ADMIN + status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE', -- ACTIVE | LOCKED | LEFT + is_del CHAR(1) DEFAULT 'N', + regdate TIMESTAMP DEFAULT NOW(), + regid TEXT, + update_date TIMESTAMP, + update_id TEXT +); +CREATE INDEX idx_users_email ON users(email); +``` + +### 4.2 제조사 / 품목 + +```sql +CREATE TABLE makers ( + objid TEXT PRIMARY KEY, + maker_name VARCHAR(200) NOT NULL, + contact VARCHAR(100), + phone VARCHAR(50), + is_del CHAR(1) DEFAULT 'N', + regdate TIMESTAMP DEFAULT NOW() +); + +CREATE TABLE items ( + objid TEXT PRIMARY KEY, + item_code VARCHAR(50) NOT NULL UNIQUE, -- 자동 생성: ITM-YYYYMMDD-#### + item_name VARCHAR(200) NOT NULL, -- 표시명 (예: "M유정란", "빨강 탈취제") + item_detail TEXT, -- 상세명 / 설명 + maker_objid TEXT REFERENCES makers(objid), + unit VARCHAR(20) DEFAULT 'EA', -- EA, BOX, KG 등 + unit_price NUMERIC(15,2) DEFAULT 0, -- 기본 단가 (대리점 출고가) + cost_price NUMERIC(15,2) DEFAULT 0, -- 매입 원가 + is_tax_free CHAR(1) DEFAULT 'N', -- 'Y' = 면세 (M 접두 품목) + image_url TEXT, -- /uploads/items/xxx.jpg + attributes JSONB, -- 자유 속성 (소비기한, 보관조건 등) + status VARCHAR(20) DEFAULT 'ACTIVE', -- ACTIVE | INACTIVE + is_del CHAR(1) DEFAULT 'N', + regdate TIMESTAMP DEFAULT NOW(), + regid TEXT, + update_date TIMESTAMP, + update_id TEXT +); +CREATE INDEX idx_items_status ON items(status, is_del); +CREATE INDEX idx_items_taxfree ON items(is_tax_free); +``` + +> **`is_tax_free` 자동 판정 보조**: 등록 화면에서 품목명이 `M`으로 시작하면 기본값을 `'Y'`로 토글 (사용자가 수정 가능). + +### 4.3 창고 / 재고 + +```sql +CREATE TABLE warehouses ( + objid TEXT PRIMARY KEY, + wh_code VARCHAR(50) NOT NULL UNIQUE, + wh_name VARCHAR(200) NOT NULL, -- 예: "본사창고", "시장픽업", "용차배송" + location VARCHAR(200), -- 위치 메모 + wh_type VARCHAR(20) DEFAULT 'STOCK', -- STOCK | PICKUP_TEAM | MARKET | DELIVERY + is_del CHAR(1) DEFAULT 'N', + regdate TIMESTAMP DEFAULT NOW() +); + +-- 창고×품목별 현재고 (스냅샷) +CREATE TABLE stocks ( + objid TEXT PRIMARY KEY, + wh_objid TEXT NOT NULL REFERENCES warehouses(objid), + item_objid TEXT NOT NULL REFERENCES items(objid), + qty NUMERIC(15,2) NOT NULL DEFAULT 0, + update_date TIMESTAMP DEFAULT NOW(), + UNIQUE(wh_objid, item_objid) +); +CREATE INDEX idx_stocks_item ON stocks(item_objid); + +-- 입출고 이력 (감사 로그) — 모든 재고 변동은 여기 기록 +CREATE TABLE stock_moves ( + objid TEXT PRIMARY KEY, + wh_objid TEXT NOT NULL, + item_objid TEXT NOT NULL, + move_type VARCHAR(20) NOT NULL, -- IN(매입입고) | OUT(출고) | ADJ(조정) | TRANSFER + qty NUMERIC(15,2) NOT NULL, -- 양수: 입고, 음수: 출고 + ref_type VARCHAR(20), -- ORDER | PROCUREMENT | MANUAL + ref_objid TEXT, -- orders.objid 또는 procurements.objid + memo TEXT, + regdate TIMESTAMP DEFAULT NOW(), + regid TEXT +); +CREATE INDEX idx_stock_moves_item ON stock_moves(item_objid, regdate); +CREATE INDEX idx_stock_moves_ref ON stock_moves(ref_type, ref_objid); +``` + +### 4.4 발주서 (대리점 → 모모유통) + +```sql +CREATE TABLE orders ( + objid TEXT PRIMARY KEY, + order_no VARCHAR(50) NOT NULL UNIQUE, -- ORD-YYYYMMDD-#### + customer_objid TEXT NOT NULL REFERENCES users(objid), -- 발주한 대리점 + order_date DATE NOT NULL DEFAULT CURRENT_DATE, + status VARCHAR(20) NOT NULL DEFAULT 'REQUESTED', + -- REQUESTED(발주요청) | APPROVED(발주완료) | SHIPPED(출고완료) + -- | INVOICED(계산서발행완료) | PAID(완납) | CANCELLED + approve_user TEXT REFERENCES users(objid), -- 승인 담당자 + approve_date TIMESTAMP, + ship_date TIMESTAMP, + invoice_no VARCHAR(50), -- 계산서 번호 + invoice_date DATE, + total_supply NUMERIC(15,2) DEFAULT 0, -- 공급가액 합계 + total_vat NUMERIC(15,2) DEFAULT 0, -- 세액 합계 + total_amount NUMERIC(15,2) DEFAULT 0, -- 총 합계 (VAT 포함) + total_taxfree NUMERIC(15,2) DEFAULT 0, -- 면세 합계 + total_taxable NUMERIC(15,2) DEFAULT 0, -- 과세 합계 (공급가) + paid_amount NUMERIC(15,2) DEFAULT 0, -- 입금액 + paid_date DATE, + memo TEXT, + is_del CHAR(1) DEFAULT 'N', + regdate TIMESTAMP DEFAULT NOW(), + regid TEXT, + update_date TIMESTAMP, + update_id TEXT +); +CREATE INDEX idx_orders_customer ON orders(customer_objid, status); +CREATE INDEX idx_orders_status_date ON orders(status, order_date); + +CREATE TABLE order_items ( + objid TEXT PRIMARY KEY, + order_objid TEXT NOT NULL REFERENCES orders(objid) ON DELETE CASCADE, + item_objid TEXT NOT NULL REFERENCES items(objid), + item_name_snap VARCHAR(200), -- 발주 시점 품목명 스냅샷 + unit_price NUMERIC(15,2) NOT NULL, -- 발주 시점 단가 + qty NUMERIC(15,2) NOT NULL, + is_tax_free CHAR(1) NOT NULL DEFAULT 'N', -- 발주 시점 면세 플래그 + supply_amount NUMERIC(15,2) NOT NULL, -- 공급가 = unit_price × qty (면세) + -- = round(unit_price × qty / 1.1) (과세) + vat_amount NUMERIC(15,2) NOT NULL DEFAULT 0,-- 세액 (면세 = 0) + total_amount NUMERIC(15,2) NOT NULL, -- 합계 = supply + vat + seq INT, + remark VARCHAR(200) +); +CREATE INDEX idx_order_items_order ON order_items(order_objid); +``` + +### 4.5 매입 발주 (모모유통 → 도매처) + +```sql +CREATE TABLE vendors ( + objid TEXT PRIMARY KEY, + vendor_name VARCHAR(200) NOT NULL, + contact VARCHAR(100), + phone VARCHAR(50), + biz_no VARCHAR(20), + is_del CHAR(1) DEFAULT 'N' +); + +CREATE TABLE procurements ( + objid TEXT PRIMARY KEY, + proc_no VARCHAR(50) NOT NULL UNIQUE, -- PRC-YYYYMMDD-#### + vendor_objid TEXT REFERENCES vendors(objid), + proc_date DATE NOT NULL DEFAULT CURRENT_DATE, + status VARCHAR(20) DEFAULT 'OPEN', -- OPEN | RECEIVED | CLOSED + total_amount NUMERIC(15,2) DEFAULT 0, + memo TEXT, + is_del CHAR(1) DEFAULT 'N', + regdate TIMESTAMP DEFAULT NOW() +); + +CREATE TABLE procurement_items ( + objid TEXT PRIMARY KEY, + proc_objid TEXT NOT NULL REFERENCES procurements(objid) ON DELETE CASCADE, + item_objid TEXT NOT NULL REFERENCES items(objid), + cost_price NUMERIC(15,2) NOT NULL, -- 매입 단가 + qty NUMERIC(15,2) NOT NULL, + total_amount NUMERIC(15,2) NOT NULL, + received_qty NUMERIC(15,2) DEFAULT 0 -- 입고 처리된 수량 +); +``` + +### 4.6 첨부 / 알림 / 메일 로그 + +```sql +CREATE TABLE attachments ( + objid TEXT PRIMARY KEY, + ref_type VARCHAR(20) NOT NULL, -- ITEM | ORDER | PROCUREMENT + ref_objid TEXT NOT NULL, + file_name VARCHAR(300) NOT NULL, + file_path TEXT NOT NULL, -- /public/uploads/... + mime_type VARCHAR(100), + file_size BIGINT, + regdate TIMESTAMP DEFAULT NOW(), + regid TEXT +); +CREATE INDEX idx_attach_ref ON attachments(ref_type, ref_objid); + +CREATE TABLE mail_logs ( + objid TEXT PRIMARY KEY, + to_email VARCHAR(200) NOT NULL, + subject VARCHAR(300), + body TEXT, + ref_type VARCHAR(20), + ref_objid TEXT, + status VARCHAR(20) DEFAULT 'PENDING', -- PENDING | SENT | FAILED + error_msg TEXT, + sent_at TIMESTAMP, + regdate TIMESTAMP DEFAULT NOW() +); +``` + +--- + +## 5. API 라우트 (Next.js Route Handler) + +> 모든 핸들러 첫 줄에 `getSession()` 검증, ADMIN 전용은 `user.role !== 'ADMIN'` 시 403. 응답 규약은 `.claude/rules/api-routes.md` 준수. + +### 5.1 인증 +| Method | Path | 설명 | +|---|---|---| +| POST | `/api/auth/signup` | 가입 (`email`, `password`, `companyName`, `ceoName?`, `bizNo?`, `phone?`) | +| POST | `/api/auth/login` | 로그인 (기존) | +| POST | `/api/auth/logout` | 로그아웃 (기존) | +| GET | `/api/auth/me` | 세션 사용자 (기존) | + +### 5.2 품목 / 제조사 (ADMIN) +| Method | Path | 설명 | +|---|---|---| +| POST | `/api/item/list` | 품목 검색 (검색어, 면세여부, 제조사, 상태) | +| POST | `/api/item/save` | 등록/수정 (`actionType`: `regist` \| `update`) | +| POST | `/api/item/delete` | 일괄 soft delete | +| POST | `/api/item/upload-image` | 이미지 업로드 → `image_url` | +| POST | `/api/maker/list` | 제조사 목록 | +| POST | `/api/maker/save` | 제조사 등록/수정 | + +### 5.3 창고 / 재고 +| Method | Path | 설명 | 권한 | +|---|---|---|---| +| POST | `/api/warehouse/list` | 창고 목록 | ADMIN | +| POST | `/api/warehouse/save` | 창고 등록/수정 | ADMIN | +| POST | `/api/inventory/list` | 현재고 (창고×품목) | 공용 (일반은 본인 가용 재고만) | +| POST | `/api/inventory/history` | 입출고 이력 | ADMIN | +| POST | `/api/inventory/inbound` | 매입 입고 등록 (재고 +, stock_moves IN 기록) | ADMIN | +| POST | `/api/inventory/adjust` | 재고 수동 조정 | ADMIN | + +### 5.4 발주서 (출고요청) +| Method | Path | 설명 | 권한 | +|---|---|---|---| +| POST | `/api/order/list` | 발주서 목록 (USER는 본인만, ADMIN은 전체) | 공용 | +| POST | `/api/order/detail` | 발주서 상세 + items | 공용 (본인/ADMIN) | +| POST | `/api/order/save` | **신규 작성/수정** (status=REQUESTED) | 공용 | +| POST | `/api/order/cancel` | 본인 발주 취소 (REQUESTED 상태에서만) | 공용 | +| POST | `/api/order/approve` | **승인 → APPROVED + 재고 차감 + 메일 발송** | ADMIN | +| POST | `/api/order/reject` | 반려 → CANCELLED | ADMIN | +| POST | `/api/order/ship` | 출고 처리 → SHIPPED | ADMIN | +| GET | `/api/order/statement/[id]` | 거래명세표 데이터 (HTML/PDF용 JSON) | 공용 (본인/ADMIN) | +| POST | `/api/order/invoice` | 계산서 발행 일괄 처리 → INVOICED | ADMIN | +| POST | `/api/order/payment` | 입금 등록 → PAID | ADMIN | + +### 5.5 매입 (조달, ADMIN) +| Method | Path | 설명 | +|---|---|---| +| POST | `/api/procurement/list` | | +| POST | `/api/procurement/save` | | +| POST | `/api/procurement/receive` | 입고 처리 → 재고 + | +| POST | `/api/vendor/list` | | +| POST | `/api/vendor/save` | | + +### 5.6 통계 (ADMIN) +| Method | Path | 설명 | +|---|---|---| +| POST | `/api/statistics/daily` | 일자별 발주 합계 (날짜 범위) | +| POST | `/api/statistics/monthly` | 월별 누적 합계 (연도) | +| POST | `/api/statistics/by-company` | 업체별 매출 (월/연도) | +| POST | `/api/statistics/by-item` | 품목별 발주 수량 (날짜 범위) | +| POST | `/api/statistics/dashboard` | 대시보드 카드용 요약 (오늘 발주, 승인 대기, 미수금, 재고 부족) | + +### 5.7 회원 관리 (ADMIN) +| Method | Path | 설명 | +|---|---|---| +| POST | `/api/admin/users/list` | | +| POST | `/api/admin/users/save` | 권한 변경, 상태 변경 | +| POST | `/api/admin/users/reset-password` | 비밀번호 초기화 | + +--- + +## 6. 핵심 워크플로우 — 발주서 라이프사이클 + +``` +[대리점] 출고요청서 작성 (status=REQUESTED) + │ + ▼ +[관리자] 발주 관리 → 승인 (POST /api/order/approve) + ├─ 1) 재고 검증: 모든 라인 qty <= 가용 재고? 실패 시 400 + ├─ 2) UPDATE orders SET status='APPROVED', approve_user, approve_date + ├─ 3) FOR EACH order_item: + │ UPDATE stocks SET qty = qty - {qty} WHERE item_objid=... + │ INSERT stock_moves(move_type='OUT', ref='ORDER', ref_objid=order) + ├─ 4) 거래명세표 HTML 생성 + ├─ 5) 메일 발송: customer.email → 거래명세표 첨부/본문 + └─ 6) mail_logs INSERT (status=SENT/FAILED) + │ + ▼ +[관리자] 출고 처리 (선택) → status=SHIPPED (포장/배송 완료) + │ + ▼ +[관리자] 월말 일괄 → 계산서 발행 (POST /api/order/invoice) + └─ status=INVOICED, invoice_no/invoice_date 채움 + │ + ▼ +[관리자] 입금 등록 → status=PAID, paid_amount, paid_date +``` + +### 6.1 트랜잭션 경계 +승인(approve)은 **단일 트랜잭션** 안에서 처리한다 — 재고 차감 실패 시 발주 상태도 롤백. + +```typescript +// 의사 코드 +await db.tx(async (tx) => { + for (const item of items) { + const stock = await tx.queryOne( + `SELECT qty FROM stocks WHERE item_objid=$1 FOR UPDATE`, [item.itemObjid] + ); + if (Number(stock.qty) < Number(item.qty)) throw new Error('재고 부족'); + await tx.execute(`UPDATE stocks SET qty = qty - $1 ...`, [...]); + await tx.execute(`INSERT INTO stock_moves (...)`, [...]); + } + await tx.execute(`UPDATE orders SET status='APPROVED', ...`); +}); +// 트랜잭션 성공 후 메일 발송 (실패해도 발주 상태는 유지, mail_logs로 추적) +``` + +### 6.2 금액 계산 규칙 +- **면세** (`is_tax_free='Y'`): `supply_amount = unit_price × qty`, `vat_amount = 0`, `total_amount = supply_amount` +- **과세** (`is_tax_free='N'`): `unit_price`가 **VAT 포함가**라고 가정 + - `total_amount = unit_price × qty` + - `supply_amount = round(total_amount / 1.1)` + - `vat_amount = total_amount - supply_amount` +- 발주 헤더 합계는 라인 합산: + - `total_supply = SUM(supply_amount)` + - `total_vat = SUM(vat_amount)` + - `total_amount = SUM(total_amount)` + - `total_taxfree = SUM(supply_amount WHERE is_tax_free='Y')` + - `total_taxable = SUM(supply_amount WHERE is_tax_free='N')` + +> 단가가 VAT-별도 모델인 경우 `items.price_mode` 컬럼을 `INCL`/`EXCL`로 추가하여 분기 (현재 스펙은 INCL 기본). + +--- + +## 7. 페이지 명세 + +### 7.1 대리점(USER) 페이지 + +#### `/dashboard` (USER 화면) +- 카드 4개: `진행중 발주 N건` / `이번달 누적 ₩` / `미수금 ₩` / `재고 알림 N건` +- 최근 발주 5건 그리드 (발주번호, 일자, 합계, 상태) +- "출고요청 작성" CTA 버튼 → `/order/form` + +#### `/order/form` — 출고요청서 작성 +- 좌측: **품목 선택 패널** + - 검색: 품목명, 제조사, 면세여부 필터 + - 그리드 컬럼: 이미지, 품목명(M표시), 제조사, 단가, **현재고**, 단위, [+ 담기] + - `현재고 = SUM(stocks.qty WHERE item_objid AND wh_type='STOCK')` — 0인 품목은 비활성화 +- 우측: **장바구니** + - 라인: 품목명, 단가, 수량(±), 합계, [삭제] + - 합계 박스: 공급가, 세액, 총합 + 면세합/과세합 + - 메모 입력 / [발주 요청] 버튼 → POST `/api/order/save` +- 저장 후 `/order/list` 리다이렉트 + 토스트 + +#### `/order/list` — 발주서 목록 +- 검색: 기간, 상태, (관리자만 업체명) +- 컬럼: 발주번호, 발주일, **업체명**, 합계, 상태(뱃지), [상세] [거래명세표] +- 행 클릭 → `/order/[id]` 상세 모달 또는 페이지 + +#### `/order/statement/[id]` — 거래명세표 +- 이미지#3 양식 재현 +- 헤더: "거래 명세 표", 발행일, 공급받는자(대리점), 공급자(모모유통) +- 본문 테이블: 순번, 품명, EA, 단가, **공급가액**, **세액**, 합계, 비고 +- 푸터: "합계 ₩{total} (VAT 포함)", 공급자 정보(계좌·전화·이메일), `momo8443@daum.net` +- 출력: 브라우저 인쇄용 CSS (A4) + PDF 다운로드 버튼 + +### 7.2 관리자(ADMIN) 페이지 + +#### `/dashboard` (ADMIN) +- KPI 카드: `오늘 발주 N건` / `승인 대기 N건` / `이번달 매출 ₩` / `미수금 ₩` / `재고 부족 품목 N개` +- 그래프 영역: + - 막대: 최근 14일 일별 발주 합계 (Recharts) + - 도넛: 이번달 면세/과세 비율 + - 막대: 업체별 이번달 매출 TOP 10 +- 위젯: + - 승인 대기 발주서 5건 (빠른 승인 버튼) + - 재고 부족 품목 (현재고 < 임계치, 임계치는 일단 10 고정) + +#### `/item/list` & `/item/form` +- 목록 컬럼: 이미지(40px), 품목코드, 품목명, 제조사, 단위, 단가, **면세여부 뱃지**, 상태 +- 등록 폼: + - 품목명, 상세설명(textarea), 제조사 선택, 단위, 단가, 원가 + - **면세여부 토글** (품목명 첫글자 `M` 입력 시 자동 ON, 사용자 변경 가능) + - 이미지 업로드 (드래그앤드롭, 단일 또는 다중) + - 속성정보 (key-value 동적 행 — JSONB로 저장: `{ "소비기한일수": 30, "보관": "냉장" }`) + +#### `/inventory/list` +- 컬럼: 창고, 품목코드, 품목명, 면세, **현재고**, 마지막 변경일 +- 검색: 창고, 품목, 면세여부 + +#### `/inventory/inbound` — 매입 입고 등록 +- 매입처(vendor) 선택 → 품목 라인 추가 → 입고 처리 +- 저장 시 `procurements` + `procurement_items` 생성, `stocks` qty 증가, `stock_moves` IN 기록 + +#### `/order/list` (관리자 뷰) +- 일반 뷰와 동일하나 **상태 변경 액션** 컬럼 추가 +- 일괄 승인 버튼 (체크박스로 선택) +- 행 우측: [승인] [반려] [출고] [계산서 발행] [입금] + +#### `/settlement/statement` — 거래명세서 일괄 +- 기간 + 업체별 필터링 → 선택된 발주 묶어서 PDF 일괄 다운로드 +- 옵션: 메일 재발송 + +#### `/settlement/invoice` — 계산서 발행 +- 미발행 발주 목록 (status=APPROVED|SHIPPED 이면서 invoice_no IS NULL) +- 업체별 그룹핑 → 한 업체의 여러 발주를 하나의 계산서로 묶음 (선택) +- 발행 시 `invoice_no` 생성 (`INV-YYYYMM-####`), 상태 INVOICED + +#### `/settlement/payment` — 입금 관리 +- 이미지#4 재현: 업체명, 총합, M포함, M미포함, **입금액**, 차액, 자동적용(완납/미납), 계산서발행여부, 입금일자 +- 행에서 입금 드롭다운으로 입금 등록 + +#### `/statistics/daily` +- 이미지#1 재현: 일자별 / 업체별 발주 수량 피벗 그리드 +- X축: 업체명, Y축: 품목명, 셀: 수량 +- 단위: 단가/발주수량/(여유분=발주-입수량) 색상 구분 + +#### `/statistics/monthly` +- 이미지#5 재현: 월별 업체별 면세매출/과세매출 피벗 +- 라인 그래프 보조: 월별 총매출 추이 (12개월) + +#### `/statistics/by-company` +- 업체별 (대리점별) 누적 매출 — 막대 + 표 + +#### `/statistics/by-item` +- 품목별 누적 발주 수량 — 막대 + 표 + +--- + +## 8. 이메일 발송 + +### 8.1 인프라 +- **라이브러리**: `nodemailer` +- **설정**: `.env`에 SMTP 정보 + ``` + SMTP_HOST=smtp.daum.net + SMTP_PORT=465 + SMTP_USER=momo8443@daum.net + SMTP_PASS=... + SMTP_FROM=모모유통 + ``` +- 송신 모듈: `src/lib/mailer.ts` — `sendOrderApprovalMail(order)` 함수 export + +### 8.2 발송 시점 +| 트리거 | 수신자 | 내용 | +|---|---|---| +| 발주 승인 (`/api/order/approve`) | `users.email` | 거래명세표 (HTML 인라인 + PDF 첨부) | +| 계산서 발행 (`/api/order/invoice`) | `users.email` | 계산서 안내 + PDF | +| 가입 환영 (선택) | 신규 가입자 | 환영 메일 | + +### 8.3 본문 템플릿 (거래명세표 예시) +``` +[모모유통] {업체명}님, 발주가 승인되었습니다. + +발주번호: {orderNo} +발주일자: {orderDate} +합계: ₩{totalAmount} (VAT 포함) + - 면세 합계: ₩{totalTaxFree} + - 과세 공급가: ₩{totalTaxable} + - 세액: ₩{totalVat} + +[품목 목록] +{각 라인} + +상세 거래명세표는 첨부 PDF를 확인하세요. +모모유통 / momo8443@daum.net / 010-6369-8443 +``` + +### 8.4 PDF 생성 +- **방안 A (권장 단순)**: 서버에서 거래명세표 HTML 렌더 → `puppeteer` 헤드리스로 PDF 캡처 +- **방안 B (가벼움)**: 클라이언트만 인쇄 + PDF는 첨부 없이 메일 본문에 링크 +- 1차 구현은 **B**, 후속으로 **A** 추가 + +### 8.5 실패 처리 +- 메일 발송 실패해도 트랜잭션은 커밋 (이미 승인된 상태) +- `mail_logs.status='FAILED'` 기록 → 관리자가 `/admin/mail-logs`(추후)에서 재시도 가능 + +--- + +## 9. 파일 업로드 + +- 저장 위치: `public/uploads/items/{yyyymm}/{uuid}.{ext}` +- 업로드 엔드포인트: `POST /api/item/upload-image` (multipart/form-data) +- 검증: 이미지 MIME만 허용 (`image/jpeg|png|webp`), 5MB 제한 +- 응답: `{ success: true, url: "/uploads/items/202604/xxx.jpg" }` +- DB: `items.image_url` 또는 `attachments` 테이블 (다중 첨부 시) + +--- + +## 10. 어드민 — 매출/원가/마진 (이미지#6 §9) + +### `/statistics/margin` (관리자 전용) +- 월별 업체별 매출 / 매입 원가 / **마진** 산출 +- 매출 = `SUM(orders.total_supply)` (면세+과세 공급가) +- 원가 = `SUM(order_items.qty × items.cost_price)` (발주 시점 원가 스냅샷이 더 정확하나 1차는 현재 원가) +- 마진 = 매출 - 원가, 마진율 = 마진 / 매출 × 100 +- 그리드 + 막대 그래프 + +> 정확한 원가 추적이 필요하면 `order_items`에 `cost_price_snap` 컬럼 추가. + +--- + +## 11. 공통 코드 / 정적 데이터 + +`code_master` (기존 FITO `code` 테이블 재사용 또는 신규)에 다음 코드그룹 등록: + +| 코드그룹 ID | 의미 | 코드 예시 | +|---|---|---| +| `ORDER_STATUS` | 발주 상태 | REQUESTED, APPROVED, SHIPPED, INVOICED, PAID, CANCELLED | +| `WH_TYPE` | 창고 유형 | STOCK, PICKUP_TEAM, MARKET, DELIVERY | +| `MOVE_TYPE` | 재고 변동 | IN, OUT, ADJ, TRANSFER | +| `UNIT` | 단위 | EA, BOX, KG, L, PACK | +| `USER_ROLE` | 권한 | USER, ADMIN | + +--- + +## 12. 권한 가드 (서버 + 클라이언트) + +### 12.1 서버 +```typescript +const user = await getSession(); +if (!user) return NextResponse.json({ success: false }, { status: 401 }); +if (user.role !== 'ADMIN') { + return NextResponse.json({ success: false, message: '권한 없음' }, { status: 403 }); +} +``` + +### 12.2 클라이언트 +- `auth-store`에 `role` 추가 → 메뉴 store에서 `role !== 'ADMIN'` 인 항목 필터링 +- ADMIN 전용 페이지: 페이지 컴포넌트 최상단에서 `if (user?.role !== 'ADMIN') redirect('/dashboard')` + +### 12.3 데이터 격리 +- `/api/order/list`에서 `USER`인 경우 `WHERE customer_objid = $session_user` +- `/api/inventory/list`에서 `USER`는 `qty > 0` 인 품목만 노출, 창고는 `STOCK` 타입만 + +--- + +## 13. 마이그레이션 / 시드 + +### 13.1 SQL 마이그레이션 파일 +`db/migrations/` 디렉토리 신규: +- `001_init_users.sql` +- `002_init_items.sql` +- `003_init_warehouse_stock.sql` +- `004_init_orders.sql` +- `005_init_procurement.sql` +- `006_init_attachments_mail.sql` +- `007_seed_codes.sql` — 공통 코드 +- `008_seed_admin.sql` — 초기 관리자 (`admin@momo.com` / 임시 비밀번호) + +> 기존 FITO 테이블은 건드리지 않고 신규 테이블만 추가. 기존 사용자가 동일 DB인 경우 충돌 회피를 위해 `momo_` 접두사 검토. + +### 13.2 초기 데이터 +- 관리자 계정 1개 +- 창고 4개 (본사창고, 시장픽업, 용차배송, 기타) +- 공통 코드 위 §11 항목 전부 + +--- + +## 14. 비기능 / 운영 + +- **성능**: 통계 조회는 `regdate` / `order_date` 인덱스 + LIMIT/OFFSET 페이징 +- **로그**: `console.error` 통일, 운영 단계에서 `pino` 도입 검토 +- **백업**: 외부 DB(`211.115.91.141:11140/fito`) 정기 백업은 인프라 책임 +- **보안**: + - 비밀번호 bcrypt 해시 (cost 10) + - JWT 만료 24h (`SESSION_TTL`) + - SQL Injection: prepared statement (`$1`...) 강제, 동적 컬럼명 금지 + - 파일 업로드 MIME/크기 검증 + 파일명 sanitize +- **i18n**: 한국어 단일 (현 단계) + +--- + +## 15. 개발 우선순위 (스프린트 가이드) + +### Sprint 1 — 기초 (1주) +1. 메뉴 정리: 불필요 디렉토리 일괄 삭제 (`bom`, `production`, `quality`, `scm`, `work`, `procurement-std`, `cs`, `fund`, `delivery`, `approval`, `cost*`, `purchase`, `sales`, `part*`, `product/*` 일부) +2. DB 스키마 마이그레이션 적용 (§4 전체) +3. 회원가입 (`/signup` + `/api/auth/signup`) +4. `users.role`, `auth-store`에 role 추가, 메뉴/페이지 권한 가드 + +### Sprint 2 — 마스터 (1주) +5. 품목 등록/목록/이미지 업로드 (`/item/*`) +6. 제조사 관리 +7. 창고 관리 + 재고 등록·조정 + 이력 + +### Sprint 3 — 발주 핵심 (1.5주) +8. 출고요청서 작성 (`/order/form`) +9. 발주서 목록 + 상세 +10. 승인 워크플로우 + 트랜잭션 재고 차감 +11. 거래명세표 페이지 (`/order/statement/[id]`) +12. 메일 발송 (nodemailer 연동) + +### Sprint 4 — 정산·통계 (1주) +13. 계산서 발행 / 입금 관리 +14. 통계 4종 (일자별·월별·업체별·품목별) +15. 대시보드 (USER + ADMIN) + +### Sprint 5 — 매입·마무리 (3일) +16. 매입(조달) 관리 + 매입 입고 → 재고 + +17. 마진 통계 +18. 메일 로그/재시도 UI +19. 시드 / 운영 가이드 정리 + +--- + +## 16. 명세 외 합의 필요 항목 (TODO 확인) + +- [ ] 가입 시 **관리자 승인** 필요 여부 (현재 자동 ACTIVE) +- [ ] 단가 모델: VAT 포함가(INCL) vs 별도(EXCL) — 현재 INCL 가정 +- [ ] 거래명세서 PDF 생성: 서버(puppeteer) vs 클라이언트(브라우저 인쇄) +- [ ] 재고 부족 임계치: 품목별 vs 전역 고정값 (현재 10 고정) +- [ ] 미수금 정의: 누적 미입금 vs 30일 초과 미입금 +- [ ] 계산서 발행 단위: 발주 1건 vs 업체별 월합산 (둘 다 가능, UI에서 선택) +- [ ] 이메일 송신 계정: `momo8443@daum.net` 비밀번호/SMTP 설정 확보 필요 + +--- + +## 17. 참고 — 원본 엑셀 워크플로우 매핑 + +| 엑셀 시트 (스크린샷) | 시스템 화면 | +|---|---| +| 시트1 — 날짜별 업체×품목 발주표 | `/statistics/daily` (피벗 그리드) | +| 시트2 — 창고/픽업팀별 분류 | `/inventory/list` (창고 필터) + `/statistics/daily` (창고 컬럼) | +| 시트3 — 거래명세표 자동생성 | `/order/statement/[id]` | +| 시트4 — 입금/계산서 체크 | `/settlement/payment` | +| 시트5 — 월간 면세/과세 매출 합산 | `/statistics/monthly` | +| 시트6 — 누적 그래프 | `/dashboard` (관리자) | +| 시트7 — 제조사 발주 본사/지사/여유분 | `/procurement/list` + `/statistics/by-item` | +| 시트8 — 제조관리(소비기한/입고가) | `items.attributes` JSONB + `/inventory/inbound` | +| 시트9 — 어드민(매출/원가/마진) | `/statistics/margin` | + +--- + +**문서 끝.** +변경/추가 요구사항은 본 문서 §16 TODO 또는 PR 코멘트로 전달. diff --git a/docs/proposal.html b/docs/proposal.html new file mode 100644 index 0000000..983353f --- /dev/null +++ b/docs/proposal.html @@ -0,0 +1,255 @@ + + + + + +모모유통 — 유통관리 시스템 제안서 + + + +
+ +
+
PROPOSAL · 2026-04-25
+

모모유통 유통관리 시스템

+
엑셀 기반 발주 업무를 웹 + 모바일 앱으로 전환합니다
+
+ 고객사 · 모모유통 + 도메인 · momo.junggomoa.com + 기간(예상) · 5주 + 플랫폼 · 웹(PC) + 안드로이드 앱 +
+
+ +
+

1 왜 이 시스템이 필요한가

+

현재는 엑셀 한 파일에 여러 명이 동시에 입력하다 보니 발주 총수량과 명세서 자동계산이 어긋나는 일이 잦습니다. 업체가 늘어날수록 단가·재고·입금 상태를 한 시트에서 관리하기가 점점 더 어렵습니다.

+
+
📋

발주 누락·중복

여러 명이 동시 편집 → 셀이 겹치거나 사라짐

+
🧮

금액 오류

VAT·면세 분리 합산이 수동, 자릿수 실수

+
📨

명세서 수작업

업체별로 매번 별도 시트 복사·메일 발송

+
📦

재고 불투명

창고별 현재고가 엑셀에 반영되지 않음

+
📊

매출 가시성 부족

월간 누적·업체별 매출을 한눈에 보기 어려움

+
📱

현장 입력 불가

거래처가 PC 앞에 가야만 발주 가능

+
+
+ +
+

2 누가 어떻게 사용하나

+ +

👤 일반 사용자 (대리점·소매상)

+
    +
  • 이메일과 업체명으로 회원가입
  • +
  • 웹 또는 안드로이드 앱에서 로그인
  • +
  • 현재 재고가 있는 품목을 검색·선택해 발주 요청
  • +
  • 본인 발주 이력·미수금·계산서 조회
  • +
+ +

🛠 관리자 (모모유통 담당자)

+
    +
  • 품목 마스터 관리 (사진·제조사·면세여부·속성)
  • +
  • 창고별 재고 등록·조정·이력 추적
  • +
  • 발주 요청 검토 → 승인 한 번으로 재고 차감 + 거래명세표 메일 자동 발송
  • +
  • 월말 계산서 발행 / 입금 관리 / 누적 매출 통계
  • +
+
+ +
+

3 핵심 업무 흐름

+
+
1단계 · 대리점
발주 요청
+
+
2단계 · 모모유통
승인 + 메일 발송
+
+
3단계 · 모모유통
출고 처리
+
+
4단계 · 월말
계산서 + 입금
+
+
+ 승인 버튼 한 번으로 다음이 자동 처리됩니다:
+ ① 재고에서 발주 수량만큼 차감 → ② 거래명세표 PDF/엑셀 자동 생성 → ③ 가입한 이메일로 명세서 본문 + 엑셀 첨부 메일 발송 → ④ 발주 상태 "발주완료"로 변경 +
+
+ +
+

4 면세 / 과세 자동 분리

+

품목명이 "M"으로 시작하는 면세 품목(예: M유정란, M꽃계탕)은 시스템이 자동으로 면세 플래그를 켜고, 거래명세표·매출통계에서 면세 합계와 과세 합계를 분리 집계합니다.

+ + + + + + + +
품명구분단가수량공급가세액합계
M 유정란면세10,00030300,000-300,000
빨강 탈취제과세9,2001192,0009,200101,200
초록 탈취제과세9,200325,0912,50927,600
+
+ +
+

5 메일 자동 발송 (거래명세표)

+

관리자가 발주를 승인하면, 가입 시 등록한 이메일로 다음과 같은 메일이 즉시 발송됩니다.

+
+
+
받는사람: 수원 거래처 <suwon@example.com>
+
제목: [모모유통] 발주 ORD-20260425-0007 승인되었습니다
+
📎 첨부 · 거래명세표.xlsx (12 KB)
+
면세 합계₩300,000
+
과세 공급가₩928,650
+
세액₩81,900
+
총 합계 (VAT 포함)₩1,310,550
+
+

메일에는 본문에 명세서 표가 포함되며, 동시에 엑셀 파일(.xlsx)이 첨부됩니다. 거래처가 그대로 회계 시스템에 올릴 수 있습니다.

+
+ +
+

6 화면 구성

+

웹 (PC) — 관리자 + 대리점 공용

+
    +
  • 대시보드 — 오늘의 발주, 승인 대기, 이번달 매출, 미수금, 재고 부족 알림
  • +
  • 품목 관리 — 사진 업로드, 제조사, 면세 여부, 속성(소비기한 등)
  • +
  • 창고/재고 — 창고별 현재고, 입출고 이력
  • +
  • 발주 관리 — 발주서 작성/목록/승인, 거래명세표 출력
  • +
  • 정산 — 계산서 발행, 입금 등록, 미수금 관리
  • +
  • 통계 — 일자별·월별·업체별·품목별 그래프
  • +
+ +

📱 안드로이드 앱 — 대리점 전용

+
    +
  • 로그인
  • +
  • 품목 검색 (사진·재고·단가 표시)
  • +
  • 장바구니 → 발주 요청
  • +
  • 내 발주 이력 + 알림
  • +
+
앱은 APK 파일로 전달드립니다. 구글 플레이 등록 없이 사내 배포 가능합니다.
+
+ +
+

7 일정 (5주)

+
+
1주차
+
기초 — 메뉴 정리, DB 구축, 회원가입, 권한
불필요한 PLM 잔재 메뉴 제거 후 모모유통 메뉴 트리로 재편
+
2주차
+
마스터 — 품목·제조사·창고·재고
품목 사진 업로드, 면세 자동 인식, 창고별 재고 관리
+
3~4주차
+
발주 핵심 — 작성·승인·메일·엑셀
장바구니 UI, 트랜잭션 재고 차감, 거래명세표 메일 + 엑셀 첨부
+
5주차
+
정산·통계·앱 + 마무리
계산서·입금·통계 화면, 안드로이드 APK 빌드, 운영 가이드
+
+
+ +
+

8 결정해 주실 사항

+
    +
  1. 가입 승인 — 거래처가 가입하면 자동 활성화? 아니면 관리자 승인 후 활성화?
  2. +
  3. 단가 모델 — 등록 단가에 VAT가 포함되어 있나요, 별도인가요? (엑셀 보면 포함으로 보입니다)
  4. +
  5. 이메일 송신 계정 — momo8443@daum.net 사용 시 SMTP 비밀번호/앱 비밀번호 필요
  6. +
  7. 계산서 발행 단위 — 발주 1건씩 / 업체별 월 합산 (둘 다 가능, 디폴트 결정 필요)
  8. +
  9. 재고 부족 알림 기준 — 임계 수량 (예: 10개 미만 알림)
  10. +
+
+ +
+

9 기대 효과

+
+

발주 처리 시간

엑셀 대비 70% 단축
(승인 1클릭 = 재고+메일+명세서)

+

금액 오류

VAT·면세 자동 계산으로
0건 목표

+
📈

매출 가시성

월간 누적·업체별 그래프를
실시간으로

+
📱

거래처 편의

휴대폰만으로 발주 가능
현장 즉시 주문

+
🔒

데이터 안전성

동시 편집 충돌 없음
모든 변경 이력 추적

+
🧾

회계 연계

엑셀 명세서 자동 첨부
거래처가 그대로 활용

+
+
+ +
+
+

고객사

+
모모유통
+
대표: ____________
+
날짜: 2026 . __ . __
+
+
+

개발사

+
chpark@wace.me
+
담당: ____________
+
날짜: 2026 . __ . __
+
+
+ +
© 2026 모모유통 유통관리 시스템 · Next.js 15 + React Native (APK) · momo.junggomoa.com
+ +
+ + diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..05e726d --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,18 @@ +import { defineConfig, globalIgnores } from "eslint/config"; +import nextVitals from "eslint-config-next/core-web-vitals"; +import nextTs from "eslint-config-next/typescript"; + +const eslintConfig = defineConfig([ + ...nextVitals, + ...nextTs, + // Override default ignores of eslint-config-next. + globalIgnores([ + // Default ignores of eslint-config-next: + ".next/**", + "out/**", + "build/**", + "next-env.d.ts", + ]), +]); + +export default eslintConfig; diff --git a/next.config.ts b/next.config.ts new file mode 100644 index 0000000..c5058e1 --- /dev/null +++ b/next.config.ts @@ -0,0 +1,11 @@ +import type { NextConfig } from "next"; +import path from "path"; + +const nextConfig: NextConfig = { + // Docker 배포용 — .next/standalone 번들 생성 (이미지 경량화) + output: "standalone", + // 프로젝트 루트 강제 고정 (상위 디렉토리 오탐 방지) + outputFileTracingRoot: path.join(__dirname), +}; + +export default nextConfig; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..7919657 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,8507 @@ +{ + "name": "fito-nextjs", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "fito-nextjs", + "version": "0.1.0", + "dependencies": { + "@prisma/client": "^7.7.0", + "@tanstack/react-table": "^8.21.3", + "@types/nodemailer": "^8.0.0", + "bcryptjs": "^3.0.3", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "date-fns": "^4.1.0", + "file-saver": "^2.0.5", + "jose": "^6.2.2", + "lucide-react": "^1.7.0", + "next": "16.2.2", + "next-auth": "^5.0.0-beta.30", + "nodemailer": "^7.0.13", + "pg": "^8.20.0", + "prisma": "^7.7.0", + "react": "19.2.4", + "react-dom": "19.2.4", + "recharts": "^3.8.1", + "sweetalert2": "^11.26.24", + "tailwind-merge": "^3.5.0", + "xlsx": "^0.18.5", + "zustand": "^5.0.12" + }, + "devDependencies": { + "@tailwindcss/postcss": "^4", + "@types/bcryptjs": "^2.4.6", + "@types/file-saver": "^2.0.7", + "@types/node": "^20", + "@types/pg": "^8.20.0", + "@types/react": "^19", + "@types/react-dom": "^19", + "eslint": "^9", + "eslint-config-next": "16.2.2", + "tailwindcss": "^4", + "typescript": "^5" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@auth/core": { + "version": "0.41.0", + "resolved": "https://registry.npmjs.org/@auth/core/-/core-0.41.0.tgz", + "integrity": "sha512-Wd7mHPQ/8zy6Qj7f4T46vg3aoor8fskJm6g2Zyj064oQ3+p0xNZXAV60ww0hY+MbTesfu29kK14Zk5d5JTazXQ==", + "license": "ISC", + "dependencies": { + "@panva/hkdf": "^1.2.1", + "jose": "^6.0.6", + "oauth4webapi": "^3.3.0", + "preact": "10.24.3", + "preact-render-to-string": "6.5.11" + }, + "peerDependencies": { + "@simplewebauthn/browser": "^9.0.1", + "@simplewebauthn/server": "^9.0.2", + "nodemailer": "^6.8.0" + }, + "peerDependenciesMeta": { + "@simplewebauthn/browser": { + "optional": true + }, + "@simplewebauthn/server": { + "optional": true + }, + "nodemailer": { + "optional": true + } + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@electric-sql/pglite": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.4.1.tgz", + "integrity": "sha512-mZ9NzzUSYPOCnxHH1oAHPRzoMFJHY472raDKwXl/+6oPbpdJ7g8LsCN4FSaIIfkiCKHhb3iF/Zqo3NYxaIhU7Q==", + "license": "Apache-2.0" + }, + "node_modules/@electric-sql/pglite-socket": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@electric-sql/pglite-socket/-/pglite-socket-0.1.1.tgz", + "integrity": "sha512-p2hoXw3Z3LQHwTeikdZNsFBOvXGqKY2hk51BBw+8NKND8eoH+8LFOtW9Z8CQKmTJ2qqGYu82ipqiyFZOTTXNfw==", + "license": "Apache-2.0", + "bin": { + "pglite-server": "dist/scripts/server.js" + }, + "peerDependencies": { + "@electric-sql/pglite": "0.4.1" + } + }, + "node_modules/@electric-sql/pglite-tools": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@electric-sql/pglite-tools/-/pglite-tools-0.3.1.tgz", + "integrity": "sha512-C+T3oivmy9bpQvSxVqXA1UDY8cB9Eb9vZHL9zxWwEUfDixbXv4G3r2LjoTdR33LD8aomR3O9ZXEO3XEwr/cUCA==", + "license": "Apache-2.0", + "peerDependencies": { + "@electric-sql/pglite": "0.4.1" + } + }, + "node_modules/@emnapi/core": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz", + "integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz", + "integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", + "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", + "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@hono/node-server": { + "version": "1.19.11", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.11.tgz", + "integrity": "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g==", + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@kurkle/color": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", + "license": "MIT" + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", + "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@tybys/wasm-util": "^0.10.0" + } + }, + "node_modules/@next/env": { + "version": "16.2.2", + "resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.2.tgz", + "integrity": "sha512-LqSGz5+xGk9EL/iBDr2yo/CgNQV6cFsNhRR2xhSXYh7B/hb4nePCxlmDvGEKG30NMHDFf0raqSyOZiQrO7BkHQ==", + "license": "MIT" + }, + "node_modules/@next/eslint-plugin-next": { + "version": "16.2.2", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-16.2.2.tgz", + "integrity": "sha512-IOPbWzDQ+76AtjZioaCjpIY72xNSDMnarZ2GMQ4wjNLvnJEJHqxQwGFhgnIWLV9klb4g/+amg88Tk5OXVpyLTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-glob": "3.3.1" + } + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "16.2.2", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.2.tgz", + "integrity": "sha512-B92G3ulrwmkDSEJEp9+XzGLex5wC1knrmCSIylyVeiAtCIfvEJYiN3v5kXPlYt5R4RFlsfO/v++aKV63Acrugg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "16.2.2", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.2.tgz", + "integrity": "sha512-7ZwSgNKJNQiwW0CKhNm9B1WS2L1Olc4B2XY0hPYCAL3epFnugMhuw5TMWzMilQ3QCZcCHoYm9NGWTHbr5REFxw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "16.2.2", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.2.tgz", + "integrity": "sha512-c3m8kBHMziMgo2fICOP/cd/5YlrxDU5YYjAJeQLyFsCqVF8xjOTH/QYG4a2u48CvvZZSj1eHQfBCbyh7kBr30Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "16.2.2", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.2.tgz", + "integrity": "sha512-VKLuscm0P/mIfzt+SDdn2+8TNNJ7f0qfEkA+az7OqQbjzKdBxAHs0UvuiVoCtbwX+dqMEL9U54b5wQ/aN3dHeg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "16.2.2", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.2.tgz", + "integrity": "sha512-kU3OPHJq6sBUjOk7wc5zJ7/lipn8yGldMoAv4z67j6ov6Xo/JvzA7L7LCsyzzsXmgLEhk3Qkpwqaq/1+XpNR3g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "16.2.2", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.2.tgz", + "integrity": "sha512-CKXRILyErMtUftp+coGcZ38ZwE/Aqq45VMCcRLr2I4OXKrgxIBDXHnBgeX/UMil0S09i2JXaDL3Q+TN8D/cKmg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "16.2.2", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.2.tgz", + "integrity": "sha512-sS/jSk5VUoShUqINJFvNjVT7JfR5ORYj/+/ZpOYbbIohv/lQfduWnGAycq2wlknbOql2xOR0DoV0s6Xfcy49+g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "16.2.2", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.2.tgz", + "integrity": "sha512-aHaKceJgdySReT7qeck5oShucxWRiiEuwCGK8HHALe6yZga8uyFpLkPgaRw3kkF04U7ROogL/suYCNt/+CuXGA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nolyfill/is-core-module": { + "version": "1.0.39", + "resolved": "https://registry.npmjs.org/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz", + "integrity": "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.4.0" + } + }, + "node_modules/@panva/hkdf": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@panva/hkdf/-/hkdf-1.2.1.tgz", + "integrity": "sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/@prisma/client": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-7.7.0.tgz", + "integrity": "sha512-5Ar4OsZpJ54s21sy5oDNNW9gQtd4NuxCaiM7+JDTOU07D6VvlpLjYzAVCMB1+JzokN+08dAVomlx+b7bhJd3ww==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/client-runtime-utils": "7.7.0" + }, + "engines": { + "node": "^20.19 || ^22.12 || >=24.0" + }, + "peerDependencies": { + "prisma": "*", + "typescript": ">=5.4.0" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/@prisma/client-runtime-utils": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@prisma/client-runtime-utils/-/client-runtime-utils-7.7.0.tgz", + "integrity": "sha512-BLyd0UpFYOtyJFTHm7jS9vesHW7P83abibodQMiIofqjBKzDHQ1VAsQkdfvXyYDkPlONPfOTz7/rv3x/+CQqvQ==", + "license": "Apache-2.0" + }, + "node_modules/@prisma/config": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@prisma/config/-/config-7.7.0.tgz", + "integrity": "sha512-hmPI3tKLO2aP0Y5vugbjcnA9qqlfJndiT6ds4tw28U5hNHLWg+mHJEWAhjsSPgxjtmxhJ/EDIeIlyh+3Us0OPg==", + "license": "Apache-2.0", + "dependencies": { + "c12": "3.1.0", + "deepmerge-ts": "7.1.5", + "effect": "3.20.0", + "empathic": "2.0.0" + } + }, + "node_modules/@prisma/debug": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.7.0.tgz", + "integrity": "sha512-12J62XdqCmpiwJHhHdQxZeY3ckVCWIFmcJP8hg5dPTceeiQ0wiojXGFYTluKqFQfu46fRLgb/rLALZMAx3+dTA==", + "license": "Apache-2.0" + }, + "node_modules/@prisma/dev": { + "version": "0.24.3", + "resolved": "https://registry.npmjs.org/@prisma/dev/-/dev-0.24.3.tgz", + "integrity": "sha512-ffHlQuKXZiaDt9Go0OnCTdJZrHxK0k7omJKNV86/VjpsXu5EIHZLK0T7JSWgvNlJwh56kW9JFu9v0qJciFzepg==", + "license": "ISC", + "dependencies": { + "@electric-sql/pglite": "0.4.1", + "@electric-sql/pglite-socket": "0.1.1", + "@electric-sql/pglite-tools": "0.3.1", + "@hono/node-server": "1.19.11", + "@prisma/get-platform": "7.2.0", + "@prisma/query-plan-executor": "7.2.0", + "@prisma/streams-local": "0.1.2", + "foreground-child": "3.3.1", + "get-port-please": "3.2.0", + "hono": "^4.12.8", + "http-status-codes": "2.3.0", + "pathe": "2.0.3", + "proper-lockfile": "4.1.2", + "remeda": "2.33.4", + "std-env": "3.10.0", + "valibot": "1.2.0", + "zeptomatch": "2.1.0" + } + }, + "node_modules/@prisma/engines": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-7.7.0.tgz", + "integrity": "sha512-7fmcbT7HHXBq/b+3h/dO1JI3fd8l8q7erf7xP7pRprh58hmSSnG8mg9K3yjW3h9WaHWUwngVFpSxxxivaitQ2w==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.7.0", + "@prisma/engines-version": "7.6.0-1.75cbdc1eb7150937890ad5465d861175c6624711", + "@prisma/fetch-engine": "7.7.0", + "@prisma/get-platform": "7.7.0" + } + }, + "node_modules/@prisma/engines-version": { + "version": "7.6.0-1.75cbdc1eb7150937890ad5465d861175c6624711", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-7.6.0-1.75cbdc1eb7150937890ad5465d861175c6624711.tgz", + "integrity": "sha512-r51DLcJ8bDRSrBEJF3J4cinoWyGA7rfP2mG6lD90VqIbGNOkbfcLcXalSVjq5Y6brQS3vcjrq4GbyUb1Cb7vkw==", + "license": "Apache-2.0" + }, + "node_modules/@prisma/engines/node_modules/@prisma/get-platform": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.7.0.tgz", + "integrity": "sha512-MEUNzvKxvYnJ7kgvd6oNRnMmmiGNS9TYLB2weMeIXplnHdL/UWEGnvavYGnN7KLJ2n0iI4dDAyzSkHI3c7AscQ==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.7.0" + } + }, + "node_modules/@prisma/fetch-engine": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-7.7.0.tgz", + "integrity": "sha512-TfyzveBQoK4xALzsTpVhB/0KG1N8zOK0ap+RnBMkzGUu3f98fnQ4QtXa2wlKPhsO2X8a3N5ugFQgcKNoHGmDfw==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.7.0", + "@prisma/engines-version": "7.6.0-1.75cbdc1eb7150937890ad5465d861175c6624711", + "@prisma/get-platform": "7.7.0" + } + }, + "node_modules/@prisma/fetch-engine/node_modules/@prisma/get-platform": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.7.0.tgz", + "integrity": "sha512-MEUNzvKxvYnJ7kgvd6oNRnMmmiGNS9TYLB2weMeIXplnHdL/UWEGnvavYGnN7KLJ2n0iI4dDAyzSkHI3c7AscQ==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.7.0" + } + }, + "node_modules/@prisma/get-platform": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.2.0.tgz", + "integrity": "sha512-k1V0l0Td1732EHpAfi2eySTezyllok9dXb6UQanajkJQzPUGi3vO2z7jdkz67SypFTdmbnyGYxvEvYZdZsMAVA==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.2.0" + } + }, + "node_modules/@prisma/get-platform/node_modules/@prisma/debug": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.2.0.tgz", + "integrity": "sha512-YSGTiSlBAVJPzX4ONZmMotL+ozJwQjRmZweQNIq/ER0tQJKJynNkRB3kyvt37eOfsbMCXk3gnLF6J9OJ4QWftw==", + "license": "Apache-2.0" + }, + "node_modules/@prisma/query-plan-executor": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@prisma/query-plan-executor/-/query-plan-executor-7.2.0.tgz", + "integrity": "sha512-EOZmNzcV8uJ0mae3DhTsiHgoNCuu1J9mULQpGCh62zN3PxPTd+qI9tJvk5jOst8WHKQNwJWR3b39t0XvfBB0WQ==", + "license": "Apache-2.0" + }, + "node_modules/@prisma/streams-local": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@prisma/streams-local/-/streams-local-0.1.2.tgz", + "integrity": "sha512-l49yTxKKF2odFxaAXTmwmkBKL3+bVQ1tFOooGifu4xkdb9NMNLxHj27XAhTylWZod8I+ISGM5erU1xcl/oBCtg==", + "license": "Apache-2.0", + "dependencies": { + "ajv": "^8.12.0", + "better-result": "^2.7.0", + "env-paths": "^3.0.0", + "proper-lockfile": "^4.1.2" + }, + "engines": { + "bun": ">=1.3.6", + "node": ">=22.0.0" + } + }, + "node_modules/@prisma/streams-local/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@prisma/streams-local/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/@prisma/studio-core": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@prisma/studio-core/-/studio-core-0.27.3.tgz", + "integrity": "sha512-AADjNFPdsrglxHQVTmHFqv6DuKQZ5WY4p5/gVFY017twvNrSwpLJ9lqUbYYxEu2W7nbvVxTZA8deJ8LseNALsw==", + "license": "Apache-2.0", + "dependencies": { + "@radix-ui/react-toggle": "1.1.10", + "chart.js": "4.5.1" + }, + "engines": { + "node": "^20.19 || ^22.12 || >=24.0", + "pnpm": "8" + }, + "peerDependencies": { + "@types/react": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.10.tgz", + "integrity": "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@reduxjs/toolkit": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz", + "integrity": "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^11.0.0", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, + "node_modules/@reduxjs/toolkit/node_modules/immer": { + "version": "11.1.4", + "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.4.tgz", + "integrity": "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, + "node_modules/@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@tailwindcss/node": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.2.tgz", + "integrity": "sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.19.0", + "jiti": "^2.6.1", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.2.2" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.2.tgz", + "integrity": "sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.2.2", + "@tailwindcss/oxide-darwin-arm64": "4.2.2", + "@tailwindcss/oxide-darwin-x64": "4.2.2", + "@tailwindcss/oxide-freebsd-x64": "4.2.2", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.2", + "@tailwindcss/oxide-linux-arm64-gnu": "4.2.2", + "@tailwindcss/oxide-linux-arm64-musl": "4.2.2", + "@tailwindcss/oxide-linux-x64-gnu": "4.2.2", + "@tailwindcss/oxide-linux-x64-musl": "4.2.2", + "@tailwindcss/oxide-wasm32-wasi": "4.2.2", + "@tailwindcss/oxide-win32-arm64-msvc": "4.2.2", + "@tailwindcss/oxide-win32-x64-msvc": "4.2.2" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.2.tgz", + "integrity": "sha512-dXGR1n+P3B6748jZO/SvHZq7qBOqqzQ+yFrXpoOWWALWndF9MoSKAT3Q0fYgAzYzGhxNYOoysRvYlpixRBBoDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.2.tgz", + "integrity": "sha512-iq9Qjr6knfMpZHj55/37ouZeykwbDqF21gPFtfnhCCKGDcPI/21FKC9XdMO/XyBM7qKORx6UIhGgg6jLl7BZlg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.2.tgz", + "integrity": "sha512-BlR+2c3nzc8f2G639LpL89YY4bdcIdUmiOOkv2GQv4/4M0vJlpXEa0JXNHhCHU7VWOKWT/CjqHdTP8aUuDJkuw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.2.tgz", + "integrity": "sha512-YUqUgrGMSu2CDO82hzlQ5qSb5xmx3RUrke/QgnoEx7KvmRJHQuZHZmZTLSuuHwFf0DJPybFMXMYf+WJdxHy/nQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.2.tgz", + "integrity": "sha512-FPdhvsW6g06T9BWT0qTwiVZYE2WIFo2dY5aCSpjG/S/u1tby+wXoslXS0kl3/KXnULlLr1E3NPRRw0g7t2kgaQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.2.tgz", + "integrity": "sha512-4og1V+ftEPXGttOO7eCmW7VICmzzJWgMx+QXAJRAhjrSjumCwWqMfkDrNu1LXEQzNAwz28NCUpucgQPrR4S2yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.2.tgz", + "integrity": "sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.2.tgz", + "integrity": "sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.2.tgz", + "integrity": "sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.2.tgz", + "integrity": "sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.8.1", + "@emnapi/runtime": "^1.8.1", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.1.1", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.2.tgz", + "integrity": "sha512-qPmaQM4iKu5mxpsrWZMOZRgZv1tOZpUm+zdhhQP0VhJfyGGO3aUKdbh3gDZc/dPLQwW4eSqWGrrcWNBZWUWaXQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.2.tgz", + "integrity": "sha512-1T/37VvI7WyH66b+vqHj/cLwnCxt7Qt3WFu5Q8hk65aOvlwAhs7rAp1VkulBJw/N4tMirXjVnylTR72uI0HGcA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/postcss": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.2.2.tgz", + "integrity": "sha512-n4goKQbW8RVXIbNKRB/45LzyUqN451deQK0nzIeauVEqjlI49slUlgKYJM2QyUzap/PcpnS7kzSUmPb1sCRvYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "@tailwindcss/node": "4.2.2", + "@tailwindcss/oxide": "4.2.2", + "postcss": "^8.5.6", + "tailwindcss": "4.2.2" + } + }, + "node_modules/@tanstack/react-table": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.21.3.tgz", + "integrity": "sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==", + "license": "MIT", + "dependencies": { + "@tanstack/table-core": "8.21.3" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@tanstack/table-core": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.21.3.tgz", + "integrity": "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/bcryptjs": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz", + "integrity": "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/file-saver": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/file-saver/-/file-saver-2.0.7.tgz", + "integrity": "sha512-dNKVfHd/jk0SkR/exKGj2ggkB45MAkzvWCaqLUUgkyjITkGNzH8H+yUwr+BLJUBjZOe9w8X3wgmXhZDRg1ED6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.39", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.39.tgz", + "integrity": "sha512-orrrD74MBUyK8jOAD/r0+lfa1I2MO6I+vAkmAWzMYbCcgrN4lCrmK52gRFQq/JRxfYPfonkr4b0jcY7Olqdqbw==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/nodemailer": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-8.0.0.tgz", + "integrity": "sha512-fyf8jWULsCo0d0BuoQ75i6IeoHs47qcqxWc7yUdUcV0pOZGjUTTOvwdG1PRXUDqN/8A64yQdQdnA2pZgcdi+cA==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/pg": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.20.0.tgz", + "integrity": "sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^2.2.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.58.1.tgz", + "integrity": "sha512-eSkwoemjo76bdXl2MYqtxg51HNwUSkWfODUOQ3PaTLZGh9uIWWFZIjyjaJnex7wXDu+TRx+ATsnSxdN9YWfRTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.58.1", + "@typescript-eslint/type-utils": "8.58.1", + "@typescript-eslint/utils": "8.58.1", + "@typescript-eslint/visitor-keys": "8.58.1", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.58.1", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.58.1.tgz", + "integrity": "sha512-gGkiNMPqerb2cJSVcruigx9eHBlLG14fSdPdqMoOcBfh+vvn4iCq2C8MzUB89PrxOXk0y3GZ1yIWb9aOzL93bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.58.1", + "@typescript-eslint/types": "8.58.1", + "@typescript-eslint/typescript-estree": "8.58.1", + "@typescript-eslint/visitor-keys": "8.58.1", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.58.1.tgz", + "integrity": "sha512-gfQ8fk6cxhtptek+/8ZIqw8YrRW5048Gug8Ts5IYcMLCw18iUgrZAEY/D7s4hkI0FxEfGakKuPK/XUMPzPxi5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.58.1", + "@typescript-eslint/types": "^8.58.1", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.58.1.tgz", + "integrity": "sha512-TPYUEqJK6avLcEjumWsIuTpuYODTTDAtoMdt8ZZa93uWMTX13Nb8L5leSje1NluammvU+oI3QRr5lLXPgihX3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.58.1", + "@typescript-eslint/visitor-keys": "8.58.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.58.1.tgz", + "integrity": "sha512-JAr2hOIct2Q+qk3G+8YFfqkqi7sC86uNryT+2i5HzMa2MPjw4qNFvtjnw1IiA1rP7QhNKVe21mSSLaSjwA1Olw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.58.1.tgz", + "integrity": "sha512-HUFxvTJVroT+0rXVJC7eD5zol6ID+Sn5npVPWoFuHGg9Ncq5Q4EYstqR+UOqaNRFXi5TYkpXXkLhoCHe3G0+7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.58.1", + "@typescript-eslint/typescript-estree": "8.58.1", + "@typescript-eslint/utils": "8.58.1", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.58.1.tgz", + "integrity": "sha512-io/dV5Aw5ezwzfPBBWLoT+5QfVtP8O7q4Kftjn5azJ88bYyp/ZMCsyW1lpKK46EXJcaYMZ1JtYj+s/7TdzmQMw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.58.1.tgz", + "integrity": "sha512-w4w7WR7GHOjqqPnvAYbazq+Y5oS68b9CzasGtnd6jIeOIeKUzYzupGTB2T4LTPSv4d+WPeccbxuneTFHYgAAWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.58.1", + "@typescript-eslint/tsconfig-utils": "8.58.1", + "@typescript-eslint/types": "8.58.1", + "@typescript-eslint/visitor-keys": "8.58.1", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.58.1.tgz", + "integrity": "sha512-Ln8R0tmWC7pTtLOzgJzYTXSCjJ9rDNHAqTaVONF4FEi2qwce8mD9iSOxOpLFFvWp/wBFlew0mjM1L1ihYWfBdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.58.1", + "@typescript-eslint/types": "8.58.1", + "@typescript-eslint/typescript-estree": "8.58.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.58.1.tgz", + "integrity": "sha512-y+vH7QE8ycjoa0bWciFg7OpFcipUuem1ujhrdLtq1gByKwfbC7bPeKsiny9e0urg93DqwGcHey+bGRKCnF1nZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.58.1", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@unrs/resolver-binding-android-arm-eabi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", + "integrity": "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-android-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz", + "integrity": "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz", + "integrity": "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz", + "integrity": "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-freebsd-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz", + "integrity": "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz", + "integrity": "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz", + "integrity": "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz", + "integrity": "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz", + "integrity": "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz", + "integrity": "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz", + "integrity": "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz", + "integrity": "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz", + "integrity": "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz", + "integrity": "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz", + "integrity": "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz", + "integrity": "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^0.2.11" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz", + "integrity": "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz", + "integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz", + "integrity": "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/adler-32": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz", + "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlast": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", + "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-shim-unscopables": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.tosorted": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ast-types-flow": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", + "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/aws-ssl-profiles": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", + "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/axe-core": { + "version": "4.11.2", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.2.tgz", + "integrity": "sha512-byD6KPdvo72y/wj2T/4zGEvvlis+PsZsn/yPS3pEO+sFpcrqRpX/TJCxvVaEsNeMrfQbCr7w163YqoD9IYwHXw==", + "dev": true, + "license": "MPL-2.0", + "engines": { + "node": ">=4" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.16", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.16.tgz", + "integrity": "sha512-Lyf3aK28zpsD1yQMiiHD4RvVb6UdMoo8xzG2XzFIfR9luPzOpcBlAsT/qfB1XWS1bxWT+UtE4WmQgsp297FYOA==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/bcryptjs": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz", + "integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==", + "license": "BSD-3-Clause", + "bin": { + "bcrypt": "bin/bcrypt" + } + }, + "node_modules/better-result": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/better-result/-/better-result-2.8.1.tgz", + "integrity": "sha512-C4FQ1gCLz1YCxmM8HhNPb4D7WQmdrdllkhNReeLwvIVtJKQFKKfwJwmM3yZEBG4P34cLtrgB+FEPr1u553hF7Q==", + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/c12": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/c12/-/c12-3.1.0.tgz", + "integrity": "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw==", + "license": "MIT", + "dependencies": { + "chokidar": "^4.0.3", + "confbox": "^0.2.2", + "defu": "^6.1.4", + "dotenv": "^16.6.1", + "exsolve": "^1.0.7", + "giget": "^2.0.0", + "jiti": "^2.4.2", + "ohash": "^2.0.11", + "pathe": "^2.0.3", + "perfect-debounce": "^1.0.0", + "pkg-types": "^2.2.0", + "rc9": "^2.1.2" + }, + "peerDependencies": { + "magicast": "^0.3.5" + }, + "peerDependenciesMeta": { + "magicast": { + "optional": true + } + } + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001787", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001787.tgz", + "integrity": "sha512-mNcrMN9KeI68u7muanUpEejSLghOKlVhRqS/Za2IeyGllJ9I9otGpR9g3nsw7n4W378TE/LyIteA0+/FOZm4Kg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/cfb": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz", + "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "crc-32": "~1.2.0" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chart.js": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz", + "integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==", + "license": "MIT", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/citty": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", + "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==", + "license": "MIT", + "dependencies": { + "consola": "^3.2.3" + } + }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "license": "MIT" + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/codepage": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz", + "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/confbox": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz", + "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==", + "license": "MIT" + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/damerau-levenshtein": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", + "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/deepmerge-ts": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz", + "integrity": "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/defu": { + "version": "6.1.7", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz", + "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==", + "license": "MIT" + }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/destr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", + "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "devOptional": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/effect": { + "version": "3.20.0", + "resolved": "https://registry.npmjs.org/effect/-/effect-3.20.0.tgz", + "integrity": "sha512-qMLfDJscrNG8p/aw+IkT9W7fgj50Z4wG5bLBy0Txsxz8iUHjDIkOgO3SV0WZfnQbNG2VJYb0b+rDLMrhM4+Krw==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "fast-check": "^3.23.1" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.332", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.332.tgz", + "integrity": "sha512-7OOtytmh/rINMLwaFTbcMVvYXO3AUm029X0LcyfYk0B557RlPkdpTpnH9+htMlfu5dKwOmT0+Zs2Aw+lnn6TeQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/empathic": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz", + "integrity": "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.20.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz", + "integrity": "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/env-paths": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz", + "integrity": "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/es-abstract": { + "version": "1.24.2", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz", + "integrity": "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-iterator-helpers": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.3.1.tgz", + "integrity": "sha512-zWwRvqWiuBPr0muUG/78cW3aHROFCNIQ3zpmYDpwdbnt2m+xlNyRWpHBpa2lJjSBit7BQ+RXA1iwbSmu5yJ/EQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.1", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.1.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.3.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "iterator.prototype": "^1.1.5", + "math-intrinsics": "^1.1.0", + "safe-array-concat": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-toolkit": { + "version": "1.45.1", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.45.1.tgz", + "integrity": "sha512-/jhoOj/Fx+A+IIyDNOvO3TItGmlMKhtX8ISAHKE90c4b/k1tqaqEZ+uUqfpU8DMnW5cgNJv606zS55jGvza0Xw==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", + "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.5", + "@eslint/js": "9.39.4", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-next": { + "version": "16.2.2", + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-16.2.2.tgz", + "integrity": "sha512-6VlvEhwoug2JpVgjZDhyXrJXUEuPY++TddzIpTaIRvlvlXXFgvQUtm3+Zr84IjFm0lXtJt73w19JA08tOaZVwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@next/eslint-plugin-next": "16.2.2", + "eslint-import-resolver-node": "^0.3.6", + "eslint-import-resolver-typescript": "^3.5.2", + "eslint-plugin-import": "^2.32.0", + "eslint-plugin-jsx-a11y": "^6.10.0", + "eslint-plugin-react": "^7.37.0", + "eslint-plugin-react-hooks": "^7.0.0", + "globals": "16.4.0", + "typescript-eslint": "^8.46.0" + }, + "peerDependencies": { + "eslint": ">=9.0.0", + "typescript": ">=3.3.1" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/eslint-config-next/node_modules/globals": { + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.4.0.tgz", + "integrity": "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.10", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.10.tgz", + "integrity": "sha512-tRrKqFyCaKict5hOd244sL6EQFNycnMQnBe+j8uqGNXYzsImGbGUU4ibtoaBmv5FLwJwcFJNeg1GeVjQfbMrDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.16.1", + "resolve": "^2.0.0-next.6" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-import-resolver-typescript": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.10.1.tgz", + "integrity": "sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@nolyfill/is-core-module": "1.0.39", + "debug": "^4.4.0", + "get-tsconfig": "^4.10.0", + "is-bun-module": "^2.0.0", + "stable-hash": "^0.0.5", + "tinyglobby": "^0.2.13", + "unrs-resolver": "^1.6.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-import-resolver-typescript" + }, + "peerDependencies": { + "eslint": "*", + "eslint-plugin-import": "*", + "eslint-plugin-import-x": "*" + }, + "peerDependenciesMeta": { + "eslint-plugin-import": { + "optional": true + }, + "eslint-plugin-import-x": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz", + "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.32.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", + "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.9", + "array.prototype.findlastindex": "^1.2.6", + "array.prototype.flat": "^1.3.3", + "array.prototype.flatmap": "^1.3.3", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.12.1", + "hasown": "^2.0.2", + "is-core-module": "^2.16.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.1", + "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.9", + "tsconfig-paths": "^3.15.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-jsx-a11y": { + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz", + "integrity": "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "aria-query": "^5.3.2", + "array-includes": "^3.1.8", + "array.prototype.flatmap": "^1.3.2", + "ast-types-flow": "^0.0.8", + "axe-core": "^4.10.0", + "axobject-query": "^4.1.0", + "damerau-levenshtein": "^1.0.8", + "emoji-regex": "^9.2.2", + "hasown": "^2.0.2", + "jsx-ast-utils": "^3.3.5", + "language-tags": "^1.0.9", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "safe-regex-test": "^1.0.3", + "string.prototype.includes": "^2.0.1" + }, + "engines": { + "node": ">=4.0" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-react": { + "version": "7.37.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", + "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.3", + "array.prototype.tosorted": "^1.1.4", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.2.1", + "estraverse": "^5.3.0", + "hasown": "^2.0.2", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.9", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.1", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.12", + "string.prototype.repeat": "^1.0.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz", + "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "license": "MIT" + }, + "node_modules/exsolve": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz", + "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==", + "license": "MIT" + }, + "node_modules/fast-check": { + "version": "3.23.2", + "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz", + "integrity": "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT", + "dependencies": { + "pure-rand": "^6.1.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", + "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/file-saver": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz", + "integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==", + "license": "MIT" + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/frac": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz", + "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "license": "MIT", + "dependencies": { + "is-property": "^1.0.2" + } + }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-port-please": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/get-port-please/-/get-port-please-3.2.0.tgz", + "integrity": "sha512-I9QVvBw5U/hw3RmWpYKRumUeaDgxTPd401x364rLmWBJcOQ753eov1eTgzDqRG9bqFIfDc7gfzcQEWrUri3o1A==", + "license": "MIT" + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.7", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.7.tgz", + "integrity": "sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/giget": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/giget/-/giget-2.0.0.tgz", + "integrity": "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==", + "license": "MIT", + "dependencies": { + "citty": "^0.1.6", + "consola": "^3.4.0", + "defu": "^6.1.4", + "node-fetch-native": "^1.6.6", + "nypm": "^0.6.0", + "pathe": "^2.0.3" + }, + "bin": { + "giget": "dist/cli.mjs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/grammex": { + "version": "3.1.12", + "resolved": "https://registry.npmjs.org/grammex/-/grammex-3.1.12.tgz", + "integrity": "sha512-6ufJOsSA7LcQehIJNCO7HIBykfM7DXQual0Ny780/DEcJIpBlHRvcqEBWGPYd7hrXL2GJ3oJI1MIhaXjWmLQOQ==", + "license": "MIT" + }, + "node_modules/graphmatch": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/graphmatch/-/graphmatch-1.1.1.tgz", + "integrity": "sha512-5ykVn/EXM1hF0XCaWh05VbYvEiOL2lY1kBxZtaYsyvjp7cmWOU1XsAdfQBwClraEofXDT197lFbXOEVMHpvQOg==", + "license": "MIT" + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/hono": { + "version": "4.12.12", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.12.tgz", + "integrity": "sha512-p1JfQMKaceuCbpJKAPKVqyqviZdS0eUxH9v82oWo1kb9xjQ5wA6iP3FNVAPDFlz5/p7d45lO+BpSk1tuSZMF4Q==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/http-status-codes": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/http-status-codes/-/http-status-codes-2.3.0.tgz", + "integrity": "sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA==", + "license": "MIT" + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/immer": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", + "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bun-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-2.0.0.tgz", + "integrity": "sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.7.1" + } + }, + "node_modules/is-bun-module/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", + "license": "MIT" + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/iterator.prototype": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", + "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "get-proto": "^1.0.0", + "has-symbols": "^1.1.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/jose": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.2.tgz", + "integrity": "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/language-subtag-registry": { + "version": "0.3.23", + "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", + "integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/language-tags": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz", + "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==", + "dev": true, + "license": "MIT", + "dependencies": { + "language-subtag-registry": "^0.3.20" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lru.min": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.4.tgz", + "integrity": "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==", + "license": "MIT", + "engines": { + "bun": ">=1.0.0", + "deno": ">=1.30.0", + "node": ">=8.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wellwelwel" + } + }, + "node_modules/lucide-react": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.7.0.tgz", + "integrity": "sha512-yI7BeItCLZJTXikmK4KNUGCKoGzSvbKlfCvw44bU4fXAL6v3gYS4uHD1jzsLkfwODYwI6Drw5Tu9Z5ulDe0TSg==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/mysql2": { + "version": "3.15.3", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.15.3.tgz", + "integrity": "sha512-FBrGau0IXmuqg4haEZRBfHNWB5mUARw6hNwPDXXGg0XzVJ50mr/9hb267lvpVMnhZ1FON3qNd4Xfcez1rbFwSg==", + "license": "MIT", + "dependencies": { + "aws-ssl-profiles": "^1.1.1", + "denque": "^2.1.0", + "generate-function": "^2.3.1", + "iconv-lite": "^0.7.0", + "long": "^5.2.1", + "lru.min": "^1.0.0", + "named-placeholders": "^1.1.3", + "seq-queue": "^0.0.5", + "sqlstring": "^2.3.2" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/named-placeholders": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.6.tgz", + "integrity": "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==", + "license": "MIT", + "dependencies": { + "lru.min": "^1.1.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/napi-postinstall": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", + "integrity": "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==", + "dev": true, + "license": "MIT", + "bin": { + "napi-postinstall": "lib/cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/napi-postinstall" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/next": { + "version": "16.2.2", + "resolved": "https://registry.npmjs.org/next/-/next-16.2.2.tgz", + "integrity": "sha512-i6AJdyVa4oQjyvX/6GeER8dpY/xlIV+4NMv/svykcLtURJSy/WzDnnUk/TM4d0uewFHK7xSQz4TbIwPgjky+3A==", + "license": "MIT", + "dependencies": { + "@next/env": "16.2.2", + "@swc/helpers": "0.5.15", + "baseline-browser-mapping": "^2.9.19", + "caniuse-lite": "^1.0.30001579", + "postcss": "8.4.31", + "styled-jsx": "5.1.6" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": ">=20.9.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "16.2.2", + "@next/swc-darwin-x64": "16.2.2", + "@next/swc-linux-arm64-gnu": "16.2.2", + "@next/swc-linux-arm64-musl": "16.2.2", + "@next/swc-linux-x64-gnu": "16.2.2", + "@next/swc-linux-x64-musl": "16.2.2", + "@next/swc-win32-arm64-msvc": "16.2.2", + "@next/swc-win32-x64-msvc": "16.2.2", + "sharp": "^0.34.5" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.51.1", + "babel-plugin-react-compiler": "*", + "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/next-auth": { + "version": "5.0.0-beta.30", + "resolved": "https://registry.npmjs.org/next-auth/-/next-auth-5.0.0-beta.30.tgz", + "integrity": "sha512-+c51gquM3F6nMVmoAusRJ7RIoY0K4Ts9HCCwyy/BRoe4mp3msZpOzYMyb5LAYc1wSo74PMQkGDcaghIO7W6Xjg==", + "license": "ISC", + "dependencies": { + "@auth/core": "0.41.0" + }, + "peerDependencies": { + "@simplewebauthn/browser": "^9.0.1", + "@simplewebauthn/server": "^9.0.2", + "next": "^14.0.0-0 || ^15.0.0 || ^16.0.0", + "nodemailer": "^7.0.7", + "react": "^18.2.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@simplewebauthn/browser": { + "optional": true + }, + "@simplewebauthn/server": { + "optional": true + }, + "nodemailer": { + "optional": true + } + } + }, + "node_modules/next/node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/node-exports-info": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.0.tgz", + "integrity": "sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "array.prototype.flatmap": "^1.3.3", + "es-errors": "^1.3.0", + "object.entries": "^1.1.9", + "semver": "^6.3.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/node-fetch-native": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", + "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.37", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz", + "integrity": "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==", + "dev": true, + "license": "MIT" + }, + "node_modules/nodemailer": { + "version": "7.0.13", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.13.tgz", + "integrity": "sha512-PNDFSJdP+KFgdsG3ZzMXCgquO7I6McjY2vlqILjtJd0hy8wEvtugS9xKRF2NWlPNGxvLCXlTNIae4serI7dinw==", + "license": "MIT-0", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/nypm": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.5.tgz", + "integrity": "sha512-K6AJy1GMVyfyMXRVB88700BJqNUkByijGJM8kEHpLdcAt+vSQAVfkWWHYzuRXHSY6xA2sNc5RjTj0p9rE2izVQ==", + "license": "MIT", + "dependencies": { + "citty": "^0.2.0", + "pathe": "^2.0.3", + "tinyexec": "^1.0.2" + }, + "bin": { + "nypm": "dist/cli.mjs" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/nypm/node_modules/citty": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.2.2.tgz", + "integrity": "sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w==", + "license": "MIT" + }, + "node_modules/oauth4webapi": { + "version": "3.8.5", + "resolved": "https://registry.npmjs.org/oauth4webapi/-/oauth4webapi-3.8.5.tgz", + "integrity": "sha512-A8jmyUckVhRJj5lspguklcl90Ydqk61H3dcU0oLhH3Yv13KpAliKTt5hknpGGPZSSfOwGyraNEFmofDYH+1kSg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.groupby": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ohash": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", + "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", + "license": "MIT" + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "license": "MIT" + }, + "node_modules/perfect-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", + "license": "MIT" + }, + "node_modules/pg": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.20.0.tgz", + "integrity": "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.12.0", + "pg-pool": "^3.13.0", + "pg-protocol": "^1.13.0", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.3.0" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.3.0.tgz", + "integrity": "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.12.0.tgz", + "integrity": "sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.13.0.tgz", + "integrity": "sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.13.0.tgz", + "integrity": "sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkg-types": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz", + "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==", + "license": "MIT", + "dependencies": { + "confbox": "^0.2.2", + "exsolve": "^1.0.7", + "pathe": "^2.0.3" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postcss": { + "version": "8.5.9", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.9.tgz", + "integrity": "sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postgres": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/postgres/-/postgres-3.4.7.tgz", + "integrity": "sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw==", + "license": "Unlicense", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/porsager" + } + }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", + "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/preact": { + "version": "10.24.3", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.24.3.tgz", + "integrity": "sha512-Z2dPnBnMUfyQfSQ+GBdsGa16hz35YmLmtTLhM169uW944hYL6xzTYkJjC07j+Wosz733pMWx0fgON3JNw1jJQA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, + "node_modules/preact-render-to-string": { + "version": "6.5.11", + "resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-6.5.11.tgz", + "integrity": "sha512-ubnauqoGczeGISiOh6RjX0/cdaF8v/oDXIjO85XALCQjwQP+SB4RDXXtvZ6yTYSjG+PC1QRP2AhPgCEsM2EvUw==", + "license": "MIT", + "peerDependencies": { + "preact": ">=10" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prisma": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-7.7.0.tgz", + "integrity": "sha512-HlgwRBt1uEFB9LStHL4HLYDvoi4BNu1rYA0hPG0zCAEyK9SaZBqp7E5Rjpc3Qh8Lex/ye/svoHZ0OWoFNhWxuQ==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/config": "7.7.0", + "@prisma/dev": "0.24.3", + "@prisma/engines": "7.7.0", + "@prisma/studio-core": "0.27.3", + "mysql2": "3.15.3", + "postgres": "3.4.7" + }, + "bin": { + "prisma": "build/index.js" + }, + "engines": { + "node": "^20.19 || ^22.12 || >=24.0" + }, + "peerDependencies": { + "better-sqlite3": ">=9.0.0", + "typescript": ">=5.4.0" + }, + "peerDependenciesMeta": { + "better-sqlite3": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/proper-lockfile": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" + } + }, + "node_modules/proper-lockfile/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/rc9": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz", + "integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==", + "license": "MIT", + "dependencies": { + "defu": "^6.1.4", + "destr": "^2.0.3" + } + }, + "node_modules/react": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.4" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/react-redux": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", + "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/recharts": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.8.1.tgz", + "integrity": "sha512-mwzmO1s9sFL0TduUpwndxCUNoXsBw3u3E/0+A+cLcrSfQitSG62L32N69GhqUrrT5qKcAE3pCGVINC6pqkBBQg==", + "license": "MIT", + "workspaces": [ + "www" + ], + "dependencies": { + "@reduxjs/toolkit": "^1.9.0 || 2.x.x", + "clsx": "^2.1.1", + "decimal.js-light": "^2.5.1", + "es-toolkit": "^1.39.3", + "eventemitter3": "^5.0.1", + "immer": "^10.1.1", + "react-redux": "8.x.x || 9.x.x", + "reselect": "5.1.1", + "tiny-invariant": "^1.3.3", + "use-sync-external-store": "^1.2.2", + "victory-vendor": "^37.0.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT" + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.0" + } + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/remeda": { + "version": "2.33.4", + "resolved": "https://registry.npmjs.org/remeda/-/remeda-2.33.4.tgz", + "integrity": "sha512-ygHswjlc/opg2VrtiYvUOPLjxjtdKvjGz1/plDhkG66hjNjFr1xmfrs2ClNFo/E6TyUFiwYNh53bKV26oBoMGQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/remeda" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", + "license": "MIT" + }, + "node_modules/resolve": { + "version": "2.0.0-next.6", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.6.tgz", + "integrity": "sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "node-exports-info": "^1.6.0", + "object-keys": "^1.1.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/seq-queue": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz", + "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==" + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, + "node_modules/sharp/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/sqlstring": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", + "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ssf": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz", + "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==", + "license": "Apache-2.0", + "dependencies": { + "frac": "~1.1.2" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/stable-hash": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", + "integrity": "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "license": "MIT" + }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string.prototype.includes": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", + "integrity": "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", + "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.3", + "set-function-name": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/styled-jsx": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", + "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", + "license": "MIT", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sweetalert2": { + "version": "11.26.24", + "resolved": "https://registry.npmjs.org/sweetalert2/-/sweetalert2-11.26.24.tgz", + "integrity": "sha512-SLgukW4wicewpW5VOukSXY5Z6DL/z7HCOK2ODSjmQPiSphCN8gJAmh9npoceXOtBRNoDN0xIz+zHYthtfiHmjg==", + "license": "MIT", + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/limonte" + } + }, + "node_modules/tailwind-merge": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.5.0.tgz", + "integrity": "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwindcss": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.2.tgz", + "integrity": "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.2.tgz", + "integrity": "sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.1.tgz", + "integrity": "sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tsconfig-paths": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/tsconfig-paths/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.58.1.tgz", + "integrity": "sha512-gf6/oHChByg9HJvhMO1iBexJh12AqqTfnuxscMDOVqfJW3htsdRJI/GfPpHTTcyeB8cSTUY2JcZmVgoyPqcrDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.58.1", + "@typescript-eslint/parser": "8.58.1", + "@typescript-eslint/typescript-estree": "8.58.1", + "@typescript-eslint/utils": "8.58.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, + "node_modules/unrs-resolver": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz", + "integrity": "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "napi-postinstall": "^0.3.0" + }, + "funding": { + "url": "https://opencollective.com/unrs-resolver" + }, + "optionalDependencies": { + "@unrs/resolver-binding-android-arm-eabi": "1.11.1", + "@unrs/resolver-binding-android-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-x64": "1.11.1", + "@unrs/resolver-binding-freebsd-x64": "1.11.1", + "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", + "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", + "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-musl": "1.11.1", + "@unrs/resolver-binding-wasm32-wasi": "1.11.1", + "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", + "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", + "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/valibot": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/valibot/-/valibot-1.2.0.tgz", + "integrity": "sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg==", + "license": "MIT", + "peerDependencies": { + "typescript": ">=5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/victory-vendor": { + "version": "37.3.6", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz", + "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.20", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", + "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/wmf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz", + "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/word": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz", + "integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/xlsx": { + "version": "0.18.5", + "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz", + "integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "cfb": "~1.2.1", + "codepage": "~1.15.0", + "crc-32": "~1.2.1", + "ssf": "~0.11.2", + "wmf": "~1.0.1", + "word": "~0.3.0" + }, + "bin": { + "xlsx": "bin/xlsx.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zeptomatch": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/zeptomatch/-/zeptomatch-2.1.0.tgz", + "integrity": "sha512-KiGErG2J0G82LSpniV0CtIzjlJ10E04j02VOudJsPyPwNZgGnRKQy7I1R7GMyg/QswnE4l7ohSGrQbQbjXPPDA==", + "license": "MIT", + "dependencies": { + "grammex": "^3.1.11", + "graphmatch": "^1.1.0" + } + }, + "node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + }, + "node_modules/zustand": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.12.tgz", + "integrity": "sha512-i77ae3aZq4dhMlRhJVCYgMLKuSiZAaUPAct2AksxQ+gOtimhGMdXljRT21P5BNpeT4kXlLIckvkPM029OljD7g==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..26f5fcb --- /dev/null +++ b/package.json @@ -0,0 +1,49 @@ +{ + "name": "fito-nextjs", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "eslint", + "migrate:momo": "node scripts/migrate-momo.mjs" + }, + "dependencies": { + "@prisma/client": "^7.7.0", + "@tanstack/react-table": "^8.21.3", + "@types/nodemailer": "^8.0.0", + "bcryptjs": "^3.0.3", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "date-fns": "^4.1.0", + "file-saver": "^2.0.5", + "jose": "^6.2.2", + "lucide-react": "^1.7.0", + "next": "16.2.2", + "next-auth": "^5.0.0-beta.30", + "nodemailer": "^7.0.13", + "pg": "^8.20.0", + "prisma": "^7.7.0", + "react": "19.2.4", + "react-dom": "19.2.4", + "recharts": "^3.8.1", + "sweetalert2": "^11.26.24", + "tailwind-merge": "^3.5.0", + "xlsx": "^0.18.5", + "zustand": "^5.0.12" + }, + "devDependencies": { + "@tailwindcss/postcss": "^4", + "@types/bcryptjs": "^2.4.6", + "@types/file-saver": "^2.0.7", + "@types/node": "^20", + "@types/pg": "^8.20.0", + "@types/react": "^19", + "@types/react-dom": "^19", + "eslint": "^9", + "eslint-config-next": "16.2.2", + "tailwindcss": "^4", + "typescript": "^5" + } +} diff --git a/postcss.config.mjs b/postcss.config.mjs new file mode 100644 index 0000000..61e3684 --- /dev/null +++ b/postcss.config.mjs @@ -0,0 +1,7 @@ +const config = { + plugins: { + "@tailwindcss/postcss": {}, + }, +}; + +export default config; diff --git a/prisma.config.ts b/prisma.config.ts new file mode 100644 index 0000000..831a20f --- /dev/null +++ b/prisma.config.ts @@ -0,0 +1,14 @@ +// This file was generated by Prisma, and assumes you have installed the following: +// npm install --save-dev prisma dotenv +import "dotenv/config"; +import { defineConfig } from "prisma/config"; + +export default defineConfig({ + schema: "prisma/schema.prisma", + migrations: { + path: "prisma/migrations", + }, + datasource: { + url: process.env["DATABASE_URL"], + }, +}); diff --git a/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 0000000..cc10721 --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,8 @@ +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} diff --git a/public/file.svg b/public/file.svg new file mode 100644 index 0000000..004145c --- /dev/null +++ b/public/file.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/globe.svg b/public/globe.svg new file mode 100644 index 0000000..567f17b --- /dev/null +++ b/public/globe.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/momo-icon.svg b/public/momo-icon.svg new file mode 100644 index 0000000..7204c6e --- /dev/null +++ b/public/momo-icon.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + M + + + + diff --git a/public/momo-logo.svg b/public/momo-logo.svg new file mode 100644 index 0000000..5e59286 --- /dev/null +++ b/public/momo-logo.svg @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + MOMO + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/next.svg b/public/next.svg new file mode 100644 index 0000000..5174b28 --- /dev/null +++ b/public/next.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/vercel.svg b/public/vercel.svg new file mode 100644 index 0000000..7705396 --- /dev/null +++ b/public/vercel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/window.svg b/public/window.svg new file mode 100644 index 0000000..b2b2a44 --- /dev/null +++ b/public/window.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/scripts/migrate-momo.mjs b/scripts/migrate-momo.mjs new file mode 100644 index 0000000..4159775 --- /dev/null +++ b/scripts/migrate-momo.mjs @@ -0,0 +1,44 @@ +// 모모유통 마이그레이션 실행 스크립트 +// 사용법: node scripts/migrate-momo.mjs +// .env.development 또는 .env.production 의 DATABASE_URL 사용 + +import pg from "pg"; +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +const envFile = process.env.NODE_ENV === "production" ? ".env.production" : ".env.development"; +const envPath = path.join(__dirname, "..", envFile); +if (fs.existsSync(envPath)) { + for (const line of fs.readFileSync(envPath, "utf-8").split(/\r?\n/)) { + const m = line.match(/^([A-Z_][A-Z0-9_]*)=(.*)$/); + if (m) process.env[m[1]] ??= m[2].replace(/^["']|["']$/g, ""); + } +} + +const dir = path.join(__dirname, "..", "db", "migrations"); +const files = fs.readdirSync(dir).filter((f) => f.endsWith(".sql")).sort(); +const conn = process.env.DATABASE_URL; +if (!conn) { + console.error("DATABASE_URL 환경변수가 설정되지 않았습니다."); + process.exit(1); +} + +const client = new pg.Client({ connectionString: conn }); +await client.connect(); +console.log(`[migrate] DB connected. Running ${files.length} files...`); +for (const f of files) { + const sql = fs.readFileSync(path.join(dir, f), "utf-8"); + console.log(` → ${f}`); + try { + await client.query(sql); + } catch (err) { + console.error(` ✖ ${f} 실패:`, err.message); + await client.end(); + process.exit(1); + } +} +console.log("[migrate] ✔ 완료"); +await client.end(); diff --git a/scripts/test-e2e.mjs b/scripts/test-e2e.mjs new file mode 100644 index 0000000..f868a89 --- /dev/null +++ b/scripts/test-e2e.mjs @@ -0,0 +1,173 @@ +// E2E 테스트: 가입 → 시드 데이터 → 발주 → 승인 + 메일 발송 +// 전제: dev 서버 실행 중 (localhost:3000), DB 마이그레이션 완료 +// 사용법: node scripts/test-e2e.mjs + +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import pg from "pg"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const envPath = path.join(__dirname, "..", ".env.development"); +if (fs.existsSync(envPath)) { + for (const line of fs.readFileSync(envPath, "utf-8").split(/\r?\n/)) { + const m = line.match(/^([A-Z_][A-Z0-9_]*)=(.*)$/); + if (m) process.env[m[1]] ??= m[2].replace(/^["']|["']$/g, ""); + } +} + +const BASE = "http://localhost:3000"; +const TEST_EMAIL = "chpark@wace.me"; +const TEST_PASSWORD = "test1234abcd"; + +let cookieJar = ""; +function setCookies(res) { + const sc = res.headers.getSetCookie?.() || res.headers.raw?.()["set-cookie"] || []; + for (const c of sc) { + const kv = c.split(";")[0]; + cookieJar = cookieJar + ? cookieJar.split("; ").filter((p) => !p.startsWith(kv.split("=")[0] + "=")).concat(kv).join("; ") + : kv; + } +} +async function api(path, init = {}) { + const res = await fetch(`${BASE}${path}`, { + ...init, + headers: { + "Content-Type": "application/json", + ...(cookieJar ? { Cookie: cookieJar } : {}), + ...(init.headers ?? {}), + }, + }); + setCookies(res); + const text = await res.text(); + let json; + try { json = JSON.parse(text); } catch { json = { raw: text }; } + return { status: res.status, body: json }; +} + +const log = (...a) => console.log("[e2e]", ...a); +const fail = (msg) => { console.error("[e2e] ✖", msg); process.exit(1); }; + +// ===== 1. DB 정리 + 시드 (테스트 데이터) ===== +log("1. DB 정리 + 시드"); +const client = new pg.Client({ connectionString: process.env.DATABASE_URL }); +await client.connect(); + +// 기존 테스트 사용자 삭제 (재실행 가능하게) +await client.query(`DELETE FROM momo_order_items WHERE order_objid IN (SELECT objid FROM momo_orders WHERE customer_objid IN (SELECT objid FROM momo_users WHERE email = $1))`, [TEST_EMAIL]); +await client.query(`DELETE FROM momo_orders WHERE customer_objid IN (SELECT objid FROM momo_users WHERE email = $1)`, [TEST_EMAIL]); +await client.query(`DELETE FROM momo_users WHERE email = $1`, [TEST_EMAIL]); + +// 품목 / 재고 시드 (테스트 품목 3개 + 본사창고 재고 100개씩) +const items = [ + { code: "TEST-EGG-01", name: "M 유정란", price: 10000, taxFree: "Y" }, + { code: "TEST-CHKN-01", name: "M 꽃계탕", price: 4500, taxFree: "Y" }, + { code: "TEST-CLEAN-01", name: "빨강 탈취제", price: 9200, taxFree: "N" }, +]; +for (const it of items) { + await client.query( + `INSERT INTO momo_items (objid, item_code, item_name, unit, unit_price, is_tax_free, status, regdate) + VALUES ($1, $2, $3, 'EA', $4, $5, 'ACTIVE', NOW()) + ON CONFLICT (item_code) DO UPDATE SET unit_price = EXCLUDED.unit_price, is_tax_free = EXCLUDED.is_tax_free, status='ACTIVE', is_del='N'`, + [`TEST-${it.code}`, it.code, it.name, it.price, it.taxFree] + ); +} +const wh = await client.query(`SELECT objid FROM momo_warehouses WHERE wh_type='STOCK' LIMIT 1`); +const whObjid = wh.rows[0].objid; +for (const it of items) { + await client.query( + `INSERT INTO momo_stocks (objid, wh_objid, item_objid, qty, update_date) + VALUES ($1, $2, (SELECT objid FROM momo_items WHERE item_code = $3), 100, NOW()) + ON CONFLICT (wh_objid, item_objid) DO UPDATE SET qty = 100, update_date = NOW()`, + [`TEST-STK-${it.code}`, whObjid, it.code] + ); +} +log(" 품목 3개 + 재고 100개씩 시드 완료"); + +// ===== 2. 가입 ===== +log("2. 회원가입"); +const sup = await api("/api/auth/signup", { + method: "POST", + body: JSON.stringify({ + email: TEST_EMAIL, + password: TEST_PASSWORD, + companyName: "테스트거래처(wace)", + ceoName: "박철현", + bizNo: "123-45-67890", + phone: "010-1234-5678", + }), +}); +if (!sup.body.success) fail(`가입 실패: ${JSON.stringify(sup.body)}`); +log(` ✔ 가입 성공 (USER 세션 발급, cookie=${cookieJar.slice(0, 30)}...)`); + +// ===== 3. 품목 조회 (USER 세션) ===== +log("3. 품목 검색 (USER)"); +const list = await api("/api/m/items/list", { method: "POST", body: JSON.stringify({ keyword: "TEST" }) }); +const visible = (list.body.RESULTLIST || []).filter((r) => r.ITEM_CODE.startsWith("TEST-")); +if (visible.length !== 3) fail(`품목 조회 실패: ${visible.length}개 (기대 3개)`); +log(` ✔ ${visible.length}개 품목 노출, 재고 ${visible[0].STOCK_QTY}`); + +// ===== 4. 발주 작성 ===== +log("4. 발주 요청"); +const order = await api("/api/m/orders/save", { + method: "POST", + body: JSON.stringify({ + lines: [ + { itemObjid: visible.find((x) => x.ITEM_CODE === "TEST-EGG-01").OBJID, qty: 30 }, + { itemObjid: visible.find((x) => x.ITEM_CODE === "TEST-CHKN-01").OBJID, qty: 20 }, + { itemObjid: visible.find((x) => x.ITEM_CODE === "TEST-CLEAN-01").OBJID, qty: 11 }, + ], + memo: "E2E 테스트 발주", + }), +}); +if (!order.body.success) fail(`발주 실패: ${JSON.stringify(order.body)}`); +log(` ✔ 발주번호: ${order.body.orderNo}, objId: ${order.body.objId}`); + +// ===== 5. 어드민 로그인 (시드 관리자) ===== +log("5. 관리자 로그인"); +cookieJar = ""; // USER 세션 클리어 +const adminLogin = await api("/api/auth/login", { + method: "POST", + body: JSON.stringify({ userId: "admin@momo.com", password: "admin1234" }), +}); +if (!adminLogin.body.success) { + // 시드 비밀번호 해시가 환경에 따라 다를 수 있어 → DB에서 비밀번호 재설정 + log(" 시드 비번 불일치 — bcrypt 재설정"); + const bcrypt = (await import("bcryptjs")).default; + const hash = await bcrypt.hash("admin1234", 10); + await client.query(`UPDATE momo_users SET password_hash = $1 WHERE email = 'admin@momo.com'`, [hash]); + const retry = await api("/api/auth/login", { + method: "POST", + body: JSON.stringify({ userId: "admin@momo.com", password: "admin1234" }), + }); + if (!retry.body.success) fail(`관리자 로그인 실패: ${JSON.stringify(retry.body)}`); +} +log(` ✔ 관리자 세션 발급`); + +// ===== 6. 승인 + 메일 발송 ===== +log("6. 발주 승인 (재고차감 + 메일발송)"); +const approve = await api("/api/m/orders/approve", { + method: "POST", + body: JSON.stringify({ objid: order.body.objId }), +}); +if (!approve.body.success) fail(`승인 실패: ${JSON.stringify(approve.body)}`); +log(` ✔ 승인 완료, mailSent=${approve.body.mailSent}, mailError=${approve.body.mailError ?? "(none)"}`); + +// ===== 7. 결과 검증 ===== +log("7. 후속 검증"); +const r1 = await client.query(`SELECT status, total_amount, total_taxfree, total_taxable FROM momo_orders WHERE objid = $1`, [order.body.objId]); +log(` 주문 상태: ${r1.rows[0].status} (기대 APPROVED)`); +log(` 금액: 면세 ${r1.rows[0].total_taxfree} / 과세 ${r1.rows[0].total_taxable} / 합계 ${r1.rows[0].total_amount}`); +if (r1.rows[0].status !== "APPROVED") fail("주문 상태 불일치"); + +const r2 = await client.query(`SELECT qty FROM momo_stocks WHERE wh_objid = $1 AND item_objid = (SELECT objid FROM momo_items WHERE item_code = 'TEST-EGG-01')`, [whObjid]); +log(` M 유정란 재고: ${r2.rows[0].qty} (기대 70 = 100-30)`); +if (Number(r2.rows[0].qty) !== 70) fail("재고 차감 불일치"); + +const r3 = await client.query(`SELECT to_email, subject, status, error_msg FROM momo_mail_logs WHERE ref_objid = $1 ORDER BY regdate DESC LIMIT 1`, [order.body.objId]); +log(` 메일 로그: to=${r3.rows[0].to_email}, status=${r3.rows[0].status}, subject=${r3.rows[0].subject}`); +if (r3.rows[0].error_msg) log(` 메일 에러: ${r3.rows[0].error_msg}`); + +log("✔ E2E 테스트 완료"); +await client.end(); diff --git a/scripts/test-smtp.mjs b/scripts/test-smtp.mjs new file mode 100644 index 0000000..e5fb57b --- /dev/null +++ b/scripts/test-smtp.mjs @@ -0,0 +1,68 @@ +// SMTP 단독 테스트 — DB 의존 없이 메일 발송 확인 +// 사용법: node scripts/test-smtp.mjs <받는사람이메일> +import nodemailer from "nodemailer"; +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const envPath = path.join(__dirname, "..", ".env.development"); +if (fs.existsSync(envPath)) { + for (const line of fs.readFileSync(envPath, "utf-8").split(/\r?\n/)) { + const m = line.match(/^([A-Z_][A-Z0-9_]*)=(.*)$/); + if (m) process.env[m[1]] ??= m[2].replace(/^["']|["']$/g, ""); + } +} + +const to = process.argv[2] || "chpark@wace.me"; +const host = process.env.SMTP_HOST; +const port = Number(process.env.SMTP_PORT || 465); +const user = process.env.SMTP_USER; +const pass = process.env.SMTP_PASS; +const from = process.env.SMTP_FROM || user; + +console.log("[smtp]", { host, port, user, from, to }); + +if (!host || !user || !pass) { + console.error("SMTP 환경변수가 누락되었습니다."); + process.exit(1); +} + +const transporter = nodemailer.createTransport({ + host, port, secure: port === 465, + auth: { user, pass }, + tls: { rejectUnauthorized: false }, + connectionTimeout: 15000, + socketTimeout: 20000, +}); + +try { + console.log("[smtp] verify connection..."); + await transporter.verify(); + console.log("[smtp] ✔ verify OK"); + + console.log("[smtp] sending test mail..."); + const info = await transporter.sendMail({ + from, + to, + subject: "[모모유통] SMTP 연결 테스트", + html: ` +
+

모모유통 메일 서버 테스트

+

이 메일이 도착했다면 SMTP 발송이 정상 작동하는 것입니다.

+
    +
  • 발송 호스트: ${host}:${port}
  • +
  • 발송 계정: ${user}
  • +
  • 발송 시각: ${new Date().toLocaleString("ko-KR")}
  • +
+

실제 발주 승인 시에는 이 메일에 거래명세표 엑셀(.xlsx) 파일이 첨부됩니다.

+
`, + text: "모모유통 SMTP 테스트 메일입니다.", + }); + console.log("[smtp] ✔ sent:", info.messageId); + console.log("[smtp] response:", info.response); +} catch (err) { + console.error("[smtp] ✖ FAILED"); + console.error(err); + process.exit(2); +} diff --git a/scripts/test-statement-mail.mjs b/scripts/test-statement-mail.mjs new file mode 100644 index 0000000..001e210 --- /dev/null +++ b/scripts/test-statement-mail.mjs @@ -0,0 +1,165 @@ +// 발주 승인 시 발송될 거래명세표 메일을 그대로 시뮬레이션 +// (DB 없이 실제 운영 코드와 동일한 buildStatementHtml/Xlsx 사용) +import nodemailer from "nodemailer"; +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import * as XLSX from "xlsx"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const envPath = path.join(__dirname, "..", ".env.development"); +if (fs.existsSync(envPath)) { + for (const line of fs.readFileSync(envPath, "utf-8").split(/\r?\n/)) { + const m = line.match(/^([A-Z_][A-Z0-9_]*)=(.*)$/); + if (m) process.env[m[1]] ??= m[2].replace(/^["']|["']$/g, ""); + } +} + +// ===== 운영 코드와 동일한 함수 (excel-statement.ts 미니 포팅) ===== +const fmt = (n) => Math.round(n); +const escapeHtml = (s) => String(s).replace(/&/g, "&").replace(//g, ">").replace(/"/g, """); +const formatNumber = (n) => n.toLocaleString("ko-KR"); + +function buildStatementXlsx(input) { + const wb = XLSX.utils.book_new(); + const aoa = []; + aoa.push(["거 래 명 세 표"]); + aoa.push([]); + aoa.push(["발주번호", input.orderNo, "", "발주일자", input.orderDate]); + aoa.push([]); + aoa.push(["[공급받는자]"]); + aoa.push(["업체명", input.customer.companyName, "대표자", input.customer.ceoName ?? "-"]); + aoa.push(["사업자번호", input.customer.bizNo ?? "-", "전화번호", input.customer.phone ?? "-"]); + aoa.push([]); + aoa.push(["[공급자]"]); + aoa.push(["업체명", input.supplier.companyName, "계좌번호", input.supplier.bankAccount ?? "-"]); + aoa.push(["전화번호", input.supplier.phone ?? "-", "이메일", input.supplier.email ?? "-"]); + aoa.push([]); + aoa.push(["순번", "품명", "구분", "수량", "단위", "단가", "공급가액", "세액", "합계"]); + for (const it of input.items) { + aoa.push([it.seq, it.itemName, it.isTaxFree ? "면세" : "과세", it.qty, it.unit || "EA", + fmt(it.unitPrice), fmt(it.supplyAmount), fmt(it.vatAmount), fmt(it.totalAmount)]); + } + aoa.push([]); + aoa.push(["", "", "", "", "", "면세 합계", fmt(input.totals.taxFree)]); + aoa.push(["", "", "", "", "", "과세 공급가", fmt(input.totals.taxable)]); + aoa.push(["", "", "", "", "", "세액 합계", fmt(input.totals.vat)]); + aoa.push(["", "", "", "", "", "총 합계 (VAT포함)", fmt(input.totals.total)]); + const ws = XLSX.utils.aoa_to_sheet(aoa); + ws["!cols"] = [{ wch: 6 }, { wch: 28 }, { wch: 6 }, { wch: 8 }, { wch: 6 }, { wch: 12 }, { wch: 14 }, { wch: 12 }, { wch: 14 }]; + ws["!merges"] = [{ s: { r: 0, c: 0 }, e: { r: 0, c: 8 } }]; + XLSX.utils.book_append_sheet(wb, ws, "거래명세표"); + return XLSX.write(wb, { type: "buffer", bookType: "xlsx" }); +} + +function buildStatementHtml(input) { + const rows = input.items.map((it) => ` + + ${it.seq} + ${escapeHtml(it.itemName)} + ${it.isTaxFree ? "면세" : "과세"} + ${it.qty} + ${escapeHtml(it.unit || "EA")} + ${formatNumber(fmt(it.unitPrice))} + ${formatNumber(fmt(it.supplyAmount))} + ${it.isTaxFree ? "-" : formatNumber(fmt(it.vatAmount))} + ${formatNumber(fmt(it.totalAmount))} + `).join(""); + return ` +

거 래 명 세 표

+
+
발주번호 ${escapeHtml(input.orderNo)}
발주일자 ${escapeHtml(input.orderDate)}
+
공급자 ${escapeHtml(input.supplier.companyName)}
+
${escapeHtml(input.supplier.phone ?? "")} · ${escapeHtml(input.supplier.email ?? "")}
+
+
+ ${escapeHtml(input.customer.companyName)} 귀하 + ${input.customer.ceoName ? ` · 대표 ${escapeHtml(input.customer.ceoName)}` : ""} + ${input.customer.bizNo ? ` · 사업자번호 ${escapeHtml(input.customer.bizNo)}` : ""} +
+ + + + + + + + + + + + + ${rows} +
순번품명구분수량단위단가공급가액세액합계
+ + + + + +
면세 합계₩ ${formatNumber(fmt(input.totals.taxFree))}
과세 공급가₩ ${formatNumber(fmt(input.totals.taxable))}
세액 합계₩ ${formatNumber(fmt(input.totals.vat))}
총 합계 (VAT 포함)₩ ${formatNumber(fmt(input.totals.total))}
+
위와 같이 계산합니다. — 모모유통
+ `; +} + +// ===== 가짜 발주 데이터 (스크린샷 첨부 거래명세표 그대로) ===== +const items = [ + { seq: 1, itemName: "M 유정란", unit: "EA", qty: 30, unitPrice: 10000, isTaxFree: true }, + { seq: 2, itemName: "M 꽃계탕", unit: "EA", qty: 20, unitPrice: 4500, isTaxFree: true }, + { seq: 3, itemName: "빨강 탈취제", unit: "EA", qty: 11, unitPrice: 9200, isTaxFree: false }, + { seq: 4, itemName: "파랑 탈취제", unit: "EA", qty: 11, unitPrice: 9200, isTaxFree: false }, + { seq: 5, itemName: "초록 탈취제", unit: "EA", qty: 3, unitPrice: 9200, isTaxFree: false }, +]; +for (const it of items) { + const total = Math.round(it.unitPrice * it.qty); + if (it.isTaxFree) { it.supplyAmount = total; it.vatAmount = 0; it.totalAmount = total; } + else { const s = Math.round(total / 1.1); it.supplyAmount = s; it.vatAmount = total - s; it.totalAmount = total; } +} +const totals = items.reduce((a, it) => ({ + supply: a.supply + it.supplyAmount, + vat: a.vat + it.vatAmount, + total: a.total + it.totalAmount, + taxFree: a.taxFree + (it.isTaxFree ? it.supplyAmount : 0), + taxable: a.taxable + (it.isTaxFree ? 0 : it.supplyAmount), +}), { supply: 0, vat: 0, total: 0, taxFree: 0, taxable: 0 }); + +const today = new Date().toISOString().slice(0, 10); +const stmt = { + orderNo: `ORD-${today.replace(/-/g, "")}-TEST`, + orderDate: today, + customer: { companyName: "수원거래처(테스트)", ceoName: "박철현", bizNo: "123-45-67890", phone: "010-1234-5678" }, + supplier: { + companyName: "모모유통", + bankAccount: process.env.MOMO_BANK_ACCOUNT, + phone: process.env.MOMO_PHONE, + email: process.env.SMTP_FROM ?? "chpark@coa-soft.com", + }, + items, totals, +}; + +const to = process.argv[2] || "chpark@wace.me"; +const port = Number(process.env.SMTP_PORT || 465); +const transporter = nodemailer.createTransport({ + host: process.env.SMTP_HOST, port, + secure: port === 465, + auth: { user: process.env.SMTP_USER, pass: process.env.SMTP_PASS }, + tls: { rejectUnauthorized: false }, +}); + +console.log("[stmt-mail] sending to:", to); +const html = buildStatementHtml(stmt); +const xlsx = buildStatementXlsx(stmt); + +const info = await transporter.sendMail({ + from: process.env.SMTP_FROM, + to, + subject: `[모모유통] 발주 ${stmt.orderNo} 승인되었습니다 (테스트)`, + html, + attachments: [{ + filename: `거래명세표_${stmt.orderNo}.xlsx`, + content: xlsx, + contentType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + }], +}); +console.log("[stmt-mail] ✔ sent:", info.messageId); +console.log("[stmt-mail] response:", info.response); +console.log("[stmt-mail] totals:", { taxFree: totals.taxFree, taxable: totals.taxable, vat: totals.vat, total: totals.total }); diff --git a/src/app/(auth)/CLAUDE.md b/src/app/(auth)/CLAUDE.md new file mode 100644 index 0000000..df77f87 --- /dev/null +++ b/src/app/(auth)/CLAUDE.md @@ -0,0 +1,16 @@ +## 역할 +비인증 사용자의 로그인 페이지. 세션 쿠키 기반 인증 후 `/dashboard`로 리다이렉트. + +## 공통 패턴 +- 단일 페이지: `login/page.tsx` (`"use client"`) +- `POST /api/auth/login` → `{ userId, password }` 전송, `{ success, message }` 응답 +- 성공: `router.push("/dashboard")`, 실패: `Swal.fire({ icon: "error" })` + +## 숨겨진 스펙 +- 다크 배경: `bg-[#1e2432]`, 흰색 폼 카드 +- 빈값 체크: 클라이언트에서만 (`!userId || !password`) +- 로딩 상태: 버튼 disabled + 텍스트 "로그인 중..." +- autoFocus: ID 필드에 자동 포커스 +- autoComplete: `"username"`, `"current-password"` 설정 + +@MISTAKES.md diff --git a/src/app/(auth)/MISTAKES.md b/src/app/(auth)/MISTAKES.md new file mode 100644 index 0000000..39ae32c --- /dev/null +++ b/src/app/(auth)/MISTAKES.md @@ -0,0 +1,9 @@ +# (auth) 오답노트 + + diff --git a/src/app/(auth)/login/page.tsx b/src/app/(auth)/login/page.tsx new file mode 100644 index 0000000..c185dd8 --- /dev/null +++ b/src/app/(auth)/login/page.tsx @@ -0,0 +1,215 @@ +"use client"; + +import { useState, FormEvent } from "react"; +import Link from "next/link"; +import { useRouter } from "next/navigation"; +import { User, Lock, Eye, EyeOff, ArrowRight, Phone, Mail, MapPin } from "lucide-react"; +import Swal from "sweetalert2"; + +export default function LoginPage() { + const router = useRouter(); + const [userId, setUserId] = useState(""); + const [password, setPassword] = useState(""); + const [showPw, setShowPw] = useState(false); + const [loading, setLoading] = useState(false); + + const handleSubmit = async (e: FormEvent) => { + e.preventDefault(); + + if (!userId || !password) { + Swal.fire({ icon: "warning", title: "아이디와 비밀번호를 입력하세요." }); + return; + } + + setLoading(true); + try { + const res = await fetch("/api/auth/login", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ userId, password }), + }); + + const data = await res.json(); + + if (data.success) { + router.push(data.redirectTo || "/dashboard"); + } else { + Swal.fire({ + icon: "error", + title: "로그인 실패", + text: data.message || "아이디 또는 비밀번호를 확인하세요.", + }); + } + } catch { + Swal.fire({ icon: "error", title: "서버 오류", text: "잠시 후 다시 시도하세요." }); + } finally { + setLoading(false); + } + }; + + return ( +
+ {/* 좌측: 브랜드 히어로 패널 */} +
+ {/* 배경 패턴 */} +
+
+ {/* 반짝이 파티클 */} +
+
+
+
+
+
+ +
+
+ MOMO + + MOMO DISTRIBUTION + +
+
+ +
+ MOMO +

+ 모모유통 유통관리 ERP +

+

+ 발주 · 입고 · 명세서 · 정산까지 —
+ 유통 업무의 모든 흐름을 한 곳에서. +

+
+ +
+
+ + 본사경기도 의왕시 벌모루길 46 B동 +
+
+ + 지사경기도 김포시 고촌읍 김포대로 451번길 210 +
+
+ 010-6624-5315 + momo8443@daum.net +
+
+
+ + {/* 우측: 로그인 폼 */} +
+
+
+
+ + WELCOME BACK +
+

로그인

+

+ 계정 정보를 입력하고 유통관리 ERP에 접속하세요. +

+
+ +
+
+ +
+ + setUserId(e.target.value)} + placeholder="아이디를 입력하세요" + autoFocus + autoComplete="username" + className="w-full h-12 pl-11 pr-4 rounded-xl border border-slate-200 bg-white text-sm text-slate-900 placeholder-slate-400 shadow-sm focus:outline-none focus:border-emerald-500 focus:ring-4 focus:ring-emerald-500/10 transition" + /> +
+
+ +
+ +
+ + setPassword(e.target.value)} + placeholder="비밀번호를 입력하세요" + autoComplete="current-password" + className="w-full h-12 pl-11 pr-12 rounded-xl border border-slate-200 bg-white text-sm text-slate-900 placeholder-slate-400 shadow-sm focus:outline-none focus:border-emerald-500 focus:ring-4 focus:ring-emerald-500/10 transition" + /> + +
+
+ + +
+ +
+

+ 아직 계정이 없으신가요?{" "} + + 회원가입 + +

+
+ +
+

+ © 2026 MOMO DISTRIBUTION. All rights reserved. +

+
+
+
+
+ ); +} diff --git a/src/app/(auth)/signup/page.tsx b/src/app/(auth)/signup/page.tsx new file mode 100644 index 0000000..9b37056 --- /dev/null +++ b/src/app/(auth)/signup/page.tsx @@ -0,0 +1,202 @@ +"use client"; + +import { useState, FormEvent } from "react"; +import Link from "next/link"; +import { useRouter } from "next/navigation"; +import Swal from "sweetalert2"; +import { Mail, Lock, Building2, User as UserIcon, Phone, FileText, ArrowRight, Eye, EyeOff } from "lucide-react"; + +export default function SignupPage() { + const router = useRouter(); + const [form, setForm] = useState({ + email: "", + password: "", + passwordConfirm: "", + companyName: "", + ceoName: "", + bizNo: "", + phone: "", + }); + const [showPw, setShowPw] = useState(false); + const [loading, setLoading] = useState(false); + + const set = (k: keyof typeof form) => (e: React.ChangeEvent) => + setForm({ ...form, [k]: e.target.value }); + + const submit = async (e: FormEvent) => { + e.preventDefault(); + if (!form.email || !form.password || !form.companyName) { + Swal.fire({ icon: "warning", title: "필수 항목을 입력하세요." }); + return; + } + if (form.password.length < 8) { + Swal.fire({ icon: "warning", title: "비밀번호는 8자 이상이어야 합니다." }); + return; + } + if (form.password !== form.passwordConfirm) { + Swal.fire({ icon: "warning", title: "비밀번호가 일치하지 않습니다." }); + return; + } + setLoading(true); + try { + const res = await fetch("/api/auth/signup", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + email: form.email, + password: form.password, + companyName: form.companyName, + ceoName: form.ceoName, + bizNo: form.bizNo, + phone: form.phone, + }), + }); + const data = await res.json(); + if (data.success) { + await Swal.fire({ + icon: "success", + title: "가입이 완료되었습니다", + text: "이제 발주를 시작하실 수 있습니다.", + confirmButtonColor: "#0f766e", + }); + router.push("/m/dashboard"); + } else { + Swal.fire({ icon: "error", title: "가입 실패", text: data.message }); + } + } catch { + Swal.fire({ icon: "error", title: "서버 오류", text: "잠시 후 다시 시도하세요." }); + } finally { + setLoading(false); + } + }; + + return ( +
+ {/* 좌측 브랜드 */} +
+
+
+ + + MOMO DISTRIBUTION + +

+ 지금 가입하고
+ 발주를 시작하세요 +

+

+ 이메일과 업체명만 입력하면 바로 사용 가능합니다. 가입 즉시 모모유통의 모든 품목을 검색하고 발주할 수 있습니다. +

+
    +
  • · 가입비 · 월 사용료 없음
  • +
  • · 안드로이드 앱 무료 제공 (APK)
  • +
  • · 거래명세표 자동 발송 (메일 + 엑셀)
  • +
+
+
+ + {/* 우측 가입 폼 */} +
+
+
+
+ + SIGN UP +
+

회원가입

+

+ 이미 계정이 있으신가요?{" "} + 로그인 +

+
+ +
+ } label="이메일 *" type="email" value={form.email} onChange={set("email")} placeholder="you@company.com" autoComplete="email" autoFocus /> +
+ +
+ + + +
+
+ } label="비밀번호 확인 *" type={showPw ? "text" : "password"} value={form.passwordConfirm} onChange={set("passwordConfirm")} placeholder="비밀번호 재입력" autoComplete="new-password" /> + } label="업체명 *" value={form.companyName} onChange={set("companyName")} placeholder="(주)거래처 또는 매장명" /> +
+ } label="대표자" value={form.ceoName} onChange={set("ceoName")} placeholder="홍길동" /> + } label="연락처" value={form.phone} onChange={set("phone")} placeholder="010-0000-0000" /> +
+ } label="사업자등록번호" value={form.bizNo} onChange={set("bizNo")} placeholder="000-00-00000 (선택)" /> + + + +

+ 가입하시면 서비스 이용약관에 동의하시는 것으로 간주됩니다. +

+ +
+
+
+ ); +} + +function Field(props: { + icon: React.ReactNode; + label: string; + value: string; + onChange: (e: React.ChangeEvent) => void; + type?: string; + placeholder?: string; + autoComplete?: string; + autoFocus?: boolean; +}) { + return ( +
+ +
+ + {props.icon} + + +
+
+ ); +} diff --git a/src/app/(main)/CLAUDE.md b/src/app/(main)/CLAUDE.md new file mode 100644 index 0000000..6c4ee3d --- /dev/null +++ b/src/app/(main)/CLAUDE.md @@ -0,0 +1,56 @@ +## 역할 +인증된 사용자 전용 업무 페이지 영역. 영업, 생산, 구매, 재고, 품질, 비용 등 70개 이상의 업무 페이지를 포함. 공통 레이아웃(Header + Sidebar + Content)으로 감싸진 대시보드 시스템. + +## 공통 패턴 + +### 페이지 구조 +모든 페이지는 `"use client"` 클라이언트 컴포넌트. 구성: +1. 제목: `

{제목}

` +2. 검색 폼: `SearchForm` + `SearchField` + `CodeSelect`/`Input` +3. 버튼 영역: 조회, 등록, 삭제 등 +4. 데이터 그리드: `DataGrid` + `Pagination` + +### 데이터 조회 +```tsx +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 || []); + } +}, [dependencies]); +``` + +### 팝업 처리 +```tsx +window.open(url, "formPopup", "width=1200,height=550,left=...,top=..."); +``` + +### 행 삭제 +```tsx +if (selectedRows.length === 0) { + Swal.fire({ icon: "warning", title: "항목을 선택하세요." }); + return; +} +``` + +## 연결 고리 +- API: `POST /api/{domain}/{resource}` (RESULTLIST 응답) +- Store: `useAuthStore()` (user, logout), `useMenuStore()` (메뉴 상태) +- 컴포넌트: DataGrid, SearchForm, SearchField, CodeSelect, Input, Button, Pagination +- 유틸: `numberWithCommas()`, `cn()` + +## 숨겨진 스펙 +- 데이터 타입: `Record[]` +- DataGrid 높이: `calc(100vh - 350px)` 패턴 (페이지별 350~400px 차감) +- 년도 필터: `Array.from({ length: 5 }, (_, i) => new Date().getFullYear() - i)` +- 셀 클릭: `col.cellClick` 콜백으로 상세 폼 팝업 +- formatter `"money"` → 통화 형식 +- JSP 원본 주석: `// salesMgmt/salesMgmtContractList.jsp 대응` +- 에러처리 미비: `if (res.ok)` 체크만, try-catch 없는 경우 많음 + +@MISTAKES.md diff --git a/src/app/(main)/MISTAKES.md b/src/app/(main)/MISTAKES.md new file mode 100644 index 0000000..b690486 --- /dev/null +++ b/src/app/(main)/MISTAKES.md @@ -0,0 +1,9 @@ +# (main) 페이지 오답노트 + + diff --git a/src/app/(main)/admin/page.tsx b/src/app/(main)/admin/page.tsx new file mode 100644 index 0000000..810d933 --- /dev/null +++ b/src/app/(main)/admin/page.tsx @@ -0,0 +1,63 @@ +"use client"; + +import { useState, useCallback, useEffect } from "react"; +import { DataGrid, type GridColumn } from "@/components/grid/data-grid"; +import { SearchForm, SearchField } from "@/components/layout/search-form"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; + +// admin/adminUserList.jsp 대응 - 관리자 (사용자관리) +export default function AdminPage() { + const [searchName, setSearchName] = useState(""); + const [searchDept, setSearchDept] = useState(""); + const [data, setData] = useState[]>([]); + + const columns: GridColumn[] = [ + { title: "사번", field: "SABUN", width: 100, hozAlign: "center" }, + { title: "아이디", field: "USER_ID", width: 120, hozAlign: "left", + cellClick: (row) => window.open(`/admin/user-form?userId=${row.USER_ID}`, "userForm", "width=700,height=600") }, + { title: "이름", field: "USER_NAME", width: 100, hozAlign: "center" }, + { title: "부서", field: "DEPT_NAME", width: 120, hozAlign: "center" }, + { title: "직급", field: "POSITION_NAME", width: 80, hozAlign: "center" }, + { title: "이메일", field: "EMAIL", width: 180, hozAlign: "left" }, + { title: "연락처", field: "TEL", width: 130, hozAlign: "center" }, + { title: "유형", field: "USER_TYPE_NAME", width: 80, hozAlign: "center" }, + { title: "권한", field: "AUTH_NAME", width: 150, hozAlign: "left" }, + ]; + + const fetchData = useCallback(async () => { + const res = await fetch("/api/admin/users", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ user_name: searchName, dept_name: searchDept }), + }); + if (res.ok) { + const json = await res.json(); + setData(json.RESULTLIST || []); + } + }, [searchName, searchDept]); + + // 페이지 진입 시 자동 로드 + useEffect(() => { fetchData(); }, [fetchData]); + + return ( +
+
+

사용자관리

+
+ + +
+
+ + + setSearchName(e.target.value)} className="w-[120px]" /> + + + setSearchDept(e.target.value)} className="w-[120px]" /> + + + +
+ ); +} diff --git a/src/app/(main)/approval/page.tsx b/src/app/(main)/approval/page.tsx new file mode 100644 index 0000000..ed30618 --- /dev/null +++ b/src/app/(main)/approval/page.tsx @@ -0,0 +1,138 @@ +"use client"; + +import { useState, useCallback, useEffect } from "react"; +import { DataGrid, type GridColumn } from "@/components/grid/data-grid"; +import { SearchForm, SearchField } from "@/components/layout/search-form"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { ApprovalStatusBadge } from "@/components/approval/ApprovalStatusBadge"; +import { TARGET_NAME_MAP } from "@/components/approval/TargetLinkMap"; +import { cn } from "@/lib/utils"; + +// approvalList.jsp 대응 - 결재함 (미결재/승인/반려/전체 탭) +type Tab = "PENDING" | "APPROVED" | "REJECTED" | ""; + +const TABS: { key: Tab; label: string }[] = [ + { key: "PENDING", label: "미결재" }, + { key: "APPROVED", label: "승인" }, + { key: "REJECTED", label: "반려" }, + { key: "", label: "전체" }, +]; + +export default function ApprovalPage() { + const [tab, setTab] = useState("PENDING"); + const [title, setTitle] = useState(""); + const [writerName, setWriterName] = useState(""); + const [fromDate, setFromDate] = useState(""); + const [toDate, setToDate] = useState(""); + const [data, setData] = useState[]>([]); + const [loading, setLoading] = useState(false); + + const openDetail = (row: Record) => { + const w = 900, h = 700; + const left = (window.screen.width - w) / 2; + const top = (window.screen.height - h) / 2; + window.open(`/approval/form?objId=${row.APPROVAL_OBJID || row.OBJID}`, + "approvalDetail", + `width=${w},height=${h},left=${left},top=${top}`); + }; + + const columns: GridColumn[] = [ + { title: "결재번호", field: "APPROVAL_NO", width: 110, hozAlign: "center" }, + { + title: "대상구분", field: "TYPE_NAME", width: 120, hozAlign: "left", + formatter: (_, row) => (TARGET_NAME_MAP[String(row.TARGET_TYPE)] || String(row.TYPE_NAME ?? row.TARGET_TYPE ?? "-")), + }, + { + title: "제목", field: "TITLE", width: 300, hozAlign: "left", + cellClick: openDetail, + }, + { title: "상신일", field: "REGDATE", width: 110, hozAlign: "center" }, + { + title: "상신자", field: "WRITER_NAME", width: 180, hozAlign: "left", + formatter: (_, row) => { + const dept = row.DEPT_NAME ? String(row.DEPT_NAME) + " / " : ""; + return dept + String(row.WRITER_NAME ?? row.WRITER ?? ""); + }, + }, + { + title: "상태", field: "STATUS_NAME", width: 100, hozAlign: "center", + formatter: (_, row) => , + }, + ]; + + const fetchData = useCallback(async () => { + setLoading(true); + try { + const res = await fetch("/api/approval", { + method: "POST", headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + status: tab, + title, + writer_name: writerName, + from_date: fromDate, + to_date: toDate, + }), + }); + if (res.ok) { + const json = await res.json(); + setData(json.RESULTLIST || []); + } + } finally { setLoading(false); } + }, [tab, title, writerName, fromDate, toDate]); + + useEffect(() => { fetchData(); }, [fetchData]); + + // 파일 업로드 팝업 등에서 refresh 콜백으로 쓸 수 있게 전역 등록 + useEffect(() => { + (window as unknown as Record).fn_search = fetchData; + return () => { delete (window as unknown as Record).fn_search; }; + }, [fetchData]); + + return ( +
+
+

결재관리

+
+ +
+
+ + {/* 탭 */} +
+ {TABS.map((t) => ( + + ))} +
+ + + + setTitle(e.target.value)} className="w-[240px]" /> + + + setWriterName(e.target.value)} className="w-[150px]" /> + + +
+ setFromDate(e.target.value)} className="w-[140px]" /> + ~ + setToDate(e.target.value)} className="w-[140px]" /> +
+
+
+ + +
+ ); +} diff --git a/src/app/(main)/bom/page.tsx b/src/app/(main)/bom/page.tsx new file mode 100644 index 0000000..fccb9bf --- /dev/null +++ b/src/app/(main)/bom/page.tsx @@ -0,0 +1,65 @@ +"use client"; + +import { useState, useCallback, useEffect } from "react"; +import { DataGrid, type GridColumn } from "@/components/grid/data-grid"; +import { SearchForm, SearchField } from "@/components/layout/search-form"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; + +// bom/bomList.jsp 대응 - BOM 관리 +export default function BomPage() { + const [productName, setProductName] = useState(""); + const [partNo, setPartNo] = useState(""); + const [data, setData] = useState[]>([]); + + const columns: GridColumn[] = [ + { title: "제품명", field: "PRODUCT_NAME", width: 180, hozAlign: "left", + cellClick: (row) => window.open(`/product/part-register/form?objId=${row.OBJID}`, "bomDetail", "width=1200,height=900") }, + { title: "제품코드", field: "PRODUCT_CODE", width: 120, hozAlign: "left" }, + { title: "Level", field: "BOM_LEVEL", width: 60, hozAlign: "center" }, + { title: "파트번호", field: "PART_NO", width: 130, hozAlign: "left" }, + { title: "파트명", field: "PART_NAME", width: 180, hozAlign: "left" }, + { title: "규격", field: "SPEC", width: 120, hozAlign: "left" }, + { title: "수량", field: "QTY", width: 70, hozAlign: "right", formatter: "money" }, + { title: "단위", field: "UNIT_NAME", width: 60, hozAlign: "center" }, + { title: "재질", field: "MATERIAL_NAME", width: 100, hozAlign: "left" }, + { title: "비고", field: "REMARK", hozAlign: "left" }, + ]; + + const fetchData = useCallback(async () => { + const res = await fetch("/api/bom", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ product_name: productName, part_no: partNo }), + }); + if (res.ok) { + const json = await res.json(); + setData(json.RESULTLIST || []); + } + }, [productName, partNo]); + + // 페이지 진입 시 자동 로드 + useEffect(() => { fetchData(); }, [fetchData]); + + return ( +
+
+

BOM 관리

+
+ +
+
+ + + + setProductName(e.target.value)} className="w-[180px]" /> + + + setPartNo(e.target.value)} className="w-[130px]" /> + + + + +
+ ); +} diff --git a/src/app/(main)/cost-mgmt/page.tsx b/src/app/(main)/cost-mgmt/page.tsx new file mode 100644 index 0000000..08137d8 --- /dev/null +++ b/src/app/(main)/cost-mgmt/page.tsx @@ -0,0 +1,65 @@ +"use client"; + +import { useState, useCallback, useEffect } from "react"; +import { DataGrid, type GridColumn } from "@/components/grid/data-grid"; +import { SearchForm, SearchField } from "@/components/layout/search-form"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; + +// costMgmt/costMgmtList.jsp 대응 - 원가관리 +export default function CostMgmtPage() { + const [year, setYear] = useState(new Date().getFullYear().toString()); + const [projectNo, setProjectNo] = useState(""); + const [data, setData] = useState[]>([]); + + const columns: GridColumn[] = [ + { title: "프로젝트번호", field: "PROJECT_NO", width: 120 }, + { title: "제품명", field: "PRODUCT_NAME", width: 180, hozAlign: "left" }, + { title: "목표원가", field: "TARGET_COST", width: 120, hozAlign: "right", formatter: "money" }, + { title: "실적원가", field: "ACTUAL_COST", width: 120, hozAlign: "right", formatter: "money" }, + { title: "차이", field: "DIFF_COST", width: 120, hozAlign: "right", formatter: "money" }, + { title: "달성율", field: "ACHIEVE_RATE", width: 80, hozAlign: "center", + formatter: (_cell, row) => `${row.ACHIEVE_RATE || 0}%` }, + { title: "등록일", field: "REGDATE", width: 100, hozAlign: "center" }, + ]; + + const fetchData = useCallback(async () => { + const res = await fetch("/api/cost-mgmt", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ year, project_no: projectNo }), + }); + if (res.ok) { + const json = await res.json(); + setData(json.RESULTLIST || []); + } + }, [year, projectNo]); + + // 페이지 진입 시 자동 로드 + useEffect(() => { fetchData(); }, [fetchData]); + + return ( +
+
+

원가관리

+
+ +
+
+ + + + + + setProjectNo(e.target.value)} className="w-[130px]" /> + + + +
+ ); +} diff --git a/src/app/(main)/cost/expense/page.tsx b/src/app/(main)/cost/expense/page.tsx new file mode 100644 index 0000000..c895d80 --- /dev/null +++ b/src/app/(main)/cost/expense/page.tsx @@ -0,0 +1,121 @@ +"use client"; + +import { useState, useCallback, useEffect } from "react"; +import Swal from "sweetalert2"; +import { DataGrid, type GridColumn } from "@/components/grid/data-grid"; +import { SearchForm, SearchField } from "@/components/layout/search-form"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { CodeSelect } from "@/components/ui/code-select"; + +// costMgmt/expenseDashBoard.jsp 대응 - 경비관리 +export default function CostExpensePage() { + const [year, setYear] = useState(new Date().getFullYear().toString()); + const [projectNo, setProjectNo] = useState(""); + const [customerObjid, setCustomerObjid] = useState(""); + const [pmUserId, setPmUserId] = useState(""); + const [data, setData] = useState[]>([]); + const [selected, setSelected] = useState[]>([]); + + const columns: GridColumn[] = [ + { title: "프로젝트번호", field: "PROJECT_NO", width: 120, hozAlign: "left", frozen: true }, + { + title: "프로젝트정보", + columns: [ + { title: "고객사", field: "CUSTOMER_NAME", width: 150, hozAlign: "left" }, + { title: "프로젝트명", field: "PROJECT_NAME", width: 200, hozAlign: "left" }, + { title: "요청납기일", field: "REQ_DEL_DATE", width: 100, hozAlign: "center" }, + { title: "셋업지", field: "SETUP", width: 100, hozAlign: "left" }, + { title: "PM", field: "PM_USER_NAME", width: 80, hozAlign: "center" }, + ], + }, + { + title: "경비현황", + columns: [ + { title: "목표가", field: "EXPENSE_COST_GOAL", width: 120, hozAlign: "right", formatter: "money" }, + { title: "발생경비", field: "TOTAL_SETTLE_AMOUNT", width: 130, hozAlign: "right", formatter: "money" }, + { title: "투입율(%)", field: "INPUT_RATE", width: 90, hozAlign: "right" }, + { title: "조립", field: "SETTLE_AMOUNT_ASSEMBLE", width: 120, hozAlign: "right", formatter: "money" }, + { title: "셋업", field: "SETTLE_AMOUNT_SETUP", width: 120, hozAlign: "right", formatter: "money" }, + { title: "외주(Turn-key)", field: "SETTLE_AMOUNT_CS", width: 130, hozAlign: "right", formatter: "money" }, + ], + }, + ]; + + const fetchData = useCallback(async () => { + const res = await fetch("/api/cost/expense", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + year, project_no: projectNo, customer_objid: customerObjid, pm_user_id: pmUserId, + }), + }); + if (res.ok) { + const json = await res.json(); + setData(json.RESULTLIST || []); + } + }, [year, projectNo, customerObjid, pmUserId]); + + useEffect(() => { fetchData(); }, [fetchData]); + + useEffect(() => { + (window as unknown as { fn_search?: () => void }).fn_search = fetchData; + return () => { delete (window as unknown as { fn_search?: () => void }).fn_search; }; + }, [fetchData]); + + const openExpenseApply = () => { + const contractObjid = selected.length === 1 ? String(selected[0].OBJID || "") : ""; + if (selected.length > 1) { + Swal.fire({ icon: "warning", title: "한번에 1개의 프로젝트만 선택 가능합니다." }); + return; + } + const w = 900, h = 600; + const left = (window.screen.width - w) / 2; + const top = (window.screen.height - h) / 2; + window.open( + `/cost/expense/apply${contractObjid ? `?contractObjid=${encodeURIComponent(contractObjid)}` : ""}`, + "expenseApply", + `width=${w},height=${h},left=${left},top=${top},scrollbars=yes,resizable=yes` + ); + }; + + return ( +
+
+

투입원가관리 - 경비관리

+
+ + +
+
+ + + + + + + setProjectNo(e.target.value)} className="w-[140px]" /> + + + + + + + + + + +
+ ); +} diff --git a/src/app/(main)/cost/labor/page.tsx b/src/app/(main)/cost/labor/page.tsx new file mode 100644 index 0000000..5a01e2d --- /dev/null +++ b/src/app/(main)/cost/labor/page.tsx @@ -0,0 +1,94 @@ +"use client"; + +import { useState, useCallback, useEffect } from "react"; +import { DataGrid, type GridColumn } from "@/components/grid/data-grid"; +import { SearchForm, SearchField } from "@/components/layout/search-form"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { CodeSelect } from "@/components/ui/code-select"; + +// costMgmt/laborCostMgmtList.jsp 대응 - 노무비관리 +export default function CostLaborPage() { + const [year, setYear] = useState(new Date().getFullYear().toString()); + const [projectNo, setProjectNo] = useState(""); + const [customerObjid, setCustomerObjid] = useState(""); + const [product, setProduct] = useState(""); + const [pmUserId, setPmUserId] = useState(""); + const [data, setData] = useState[]>([]); + + const columns: GridColumn[] = [ + { title: "프로젝트번호", field: "PROJECT_NO", width: 120, hozAlign: "left", frozen: true }, + { + title: "프로젝트정보", + columns: [ + { title: "고객사", field: "CUSTOMER_NAME", width: 150, hozAlign: "left" }, + { title: "프로젝트명", field: "PROJECT_NAME", width: 200, hozAlign: "left" }, + { title: "고객납기일", field: "REQ_DEL_DATE", width: 100, hozAlign: "center" }, + { title: "셋업지", field: "SETUP", width: 100, hozAlign: "left" }, + { title: "PM", field: "PM_USER_NAME", width: 80, hozAlign: "center" }, + ], + }, + { + title: "노무비현황", + columns: [ + { title: "목표가", field: "LABOR_COST_GOAL", width: 120, hozAlign: "right", formatter: "money" }, + { title: "실투입노무비", field: "LABOR_COST_ACTUAL", width: 120, hozAlign: "right", formatter: "money" }, + { title: "투입율(%)", field: "LABOR_INPUT_RATE", width: 90, hozAlign: "right" }, + { title: "투입공수(H)", field: "LABOR_HOURS", width: 100, hozAlign: "right" }, + ], + }, + ]; + + const fetchData = useCallback(async () => { + const res = await fetch("/api/cost/labor", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + year, project_no: projectNo, customer_objid: customerObjid, + product, pm_user_id: pmUserId, + }), + }); + if (res.ok) { + const json = await res.json(); + setData(json.RESULTLIST || []); + } + }, [year, projectNo, customerObjid, product, pmUserId]); + + useEffect(() => { fetchData(); }, [fetchData]); + + return ( +
+
+

투입원가관리 - 노무비관리

+
+ +
+
+ + + + + + + setProjectNo(e.target.value)} className="w-[140px]" /> + + + + + + + + + + + + + +
+ ); +} diff --git a/src/app/(main)/cost/material/page.tsx b/src/app/(main)/cost/material/page.tsx new file mode 100644 index 0000000..2968b70 --- /dev/null +++ b/src/app/(main)/cost/material/page.tsx @@ -0,0 +1,95 @@ +"use client"; + +import { useState, useCallback, useEffect } from "react"; +import { DataGrid, type GridColumn } from "@/components/grid/data-grid"; +import { SearchForm, SearchField } from "@/components/layout/search-form"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { CodeSelect } from "@/components/ui/code-select"; + +// costMgmt/materialCostTotaltList.jsp 대응 - 재료비관리 +export default function CostMaterialPage() { + const [year, setYear] = useState(new Date().getFullYear().toString()); + const [projectNo, setProjectNo] = useState(""); + const [customerObjid, setCustomerObjid] = useState(""); + const [product, setProduct] = useState(""); + const [pmUserId, setPmUserId] = useState(""); + const [data, setData] = useState[]>([]); + + const columns: GridColumn[] = [ + { title: "프로젝트번호", field: "PROJECT_NO", width: 120, hozAlign: "left", frozen: true }, + { + title: "프로젝트정보", + columns: [ + { title: "고객사", field: "CUSTOMER_NAME", width: 150, hozAlign: "left" }, + { title: "프로젝트명", field: "PROJECT_NAME", width: 200, hozAlign: "left" }, + { title: "고객납기일", field: "REQ_DEL_DATE", width: 100, hozAlign: "center" }, + { title: "셋업지", field: "SETUP", width: 100, hozAlign: "left" }, + { title: "PM", field: "PM_USER_NAME", width: 80, hozAlign: "center" }, + ], + }, + { + title: "재료비현황", + columns: [ + { title: "목표가", field: "MATERIAL_COST_GOAL", width: 120, hozAlign: "right", formatter: "money" }, + { title: "실투입재료비", field: "ALL_TOTAL_PRICE", width: 130, hozAlign: "right", formatter: "money" }, + { title: "투입율(%)", field: "MATERIAL_COST_GOAL_RATE", width: 90, hozAlign: "right" }, + { title: "발주금액", field: "NEW_TOTAL_PRICE", width: 120, hozAlign: "right", formatter: "money" }, + { title: "재발주금액", field: "ALL_TOTAL_PRICE_RE", width: 120, hozAlign: "right", formatter: "money" }, + ], + }, + ]; + + const fetchData = useCallback(async () => { + const res = await fetch("/api/cost/material", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + year, project_no: projectNo, customer_objid: customerObjid, + product, pm_user_id: pmUserId, + }), + }); + if (res.ok) { + const json = await res.json(); + setData(json.RESULTLIST || []); + } + }, [year, projectNo, customerObjid, product, pmUserId]); + + useEffect(() => { fetchData(); }, [fetchData]); + + return ( +
+
+

투입원가관리 - 재료비관리

+
+ +
+
+ + + + + + + setProjectNo(e.target.value)} className="w-[140px]" /> + + + + + + + + + + + + + +
+ ); +} diff --git a/src/app/(main)/cost/status/page.tsx b/src/app/(main)/cost/status/page.tsx new file mode 100644 index 0000000..96d61bc --- /dev/null +++ b/src/app/(main)/cost/status/page.tsx @@ -0,0 +1,155 @@ +"use client"; + +import { useState, useCallback, useEffect } from "react"; +import Swal from "sweetalert2"; +import { DataGrid, type GridColumn } from "@/components/grid/data-grid"; +import { SearchForm, SearchField } from "@/components/layout/search-form"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { CodeSelect } from "@/components/ui/code-select"; + +// costMgmt/costTotaltList.jsp 대응 - 투입원가관리 현황 +export default function CostStatusPage() { + const [year, setYear] = useState(new Date().getFullYear().toString()); + const [projectNo, setProjectNo] = useState(""); + const [customerObjid, setCustomerObjid] = useState(""); + const [product, setProduct] = useState(""); + const [pmUserId, setPmUserId] = useState(""); + const [data, setData] = useState[]>([]); + const [selected, setSelected] = useState[]>([]); + + const columns: GridColumn[] = [ + { title: "프로젝트번호", field: "PROJECT_NO", width: 120, hozAlign: "left", frozen: true }, + { + title: "프로젝트정보", + columns: [ + { title: "고객사", field: "CUSTOMER_NAME", width: 140, hozAlign: "left" }, + { title: "프로젝트명", field: "PROJECT_NAME", width: 180, hozAlign: "left" }, + { title: "요청납기일", field: "REQ_DEL_DATE", width: 100, hozAlign: "center" }, + { title: "셋업지", field: "SETUP", width: 100, hozAlign: "left" }, + { title: "PM", field: "PM_USER_NAME", width: 80, hozAlign: "center" }, + ], + }, + { + title: "투입원가현황", + columns: [ + { title: "수주가", field: "CONTRACT_PRICE", width: 110, hozAlign: "right", formatter: "money" }, + { title: "목표가", field: "TOTAL_COST_GOAL", width: 110, hozAlign: "right", formatter: "money" }, + { title: "실투입원가", field: "TOTAL_COST_ACTUAL", width: 110, hozAlign: "right", formatter: "money" }, + { title: "투입율(%)", field: "TOTAL_INPUT_RATE", width: 80, hozAlign: "right" }, + { title: "MC율(%)", field: "MC_RATE", width: 80, hozAlign: "right" }, + ], + }, + { + title: "재료비현황", + columns: [ + { title: "목표가", field: "MATERIAL_COST_GOAL", width: 110, hozAlign: "right", formatter: "money" }, + { title: "발생재료비", field: "ACCRUAL_MATERIAL_COST", width: 110, hozAlign: "right", formatter: "money" }, + { title: "투입율(%)", field: "MATERIAL_COST_GOAL_RATE", width: 90, hozAlign: "right" }, + ], + }, + { + title: "노무비현황", + columns: [ + { title: "목표가", field: "LABOR_COST_GOAL", width: 110, hozAlign: "right", formatter: "money" }, + { title: "발생노무비", field: "LABOR_COST_ACTUAL", width: 110, hozAlign: "right", formatter: "money" }, + { title: "투입율(%)", field: "LABOR_INPUT_RATE", width: 90, hozAlign: "right" }, + ], + }, + { + title: "경비현황", + columns: [ + { title: "목표가", field: "EXPENSE_COST_GOAL", width: 110, hozAlign: "right", formatter: "money" }, + { title: "발생경비", field: "ACCRUAL_EXPENSE", width: 110, hozAlign: "right", formatter: "money" }, + { title: "투입율(%)", field: "EXPENSE_RATE", width: 90, hozAlign: "right" }, + ], + }, + ]; + + const fetchData = useCallback(async () => { + const res = await fetch("/api/cost/status", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + year, project_no: projectNo, customer_objid: customerObjid, + product, pm_user_id: pmUserId, + }), + }); + if (res.ok) { + const json = await res.json(); + setData(json.RESULTLIST || []); + } + }, [year, projectNo, customerObjid, product, pmUserId]); + + useEffect(() => { fetchData(); }, [fetchData]); + + // 팝업 저장 후 새로고침 + useEffect(() => { + (window as unknown as { fn_search?: () => void }).fn_search = fetchData; + return () => { delete (window as unknown as { fn_search?: () => void }).fn_search; }; + }, [fetchData]); + + const openGoalPopup = () => { + if (selected.length === 0) { + Swal.fire({ icon: "warning", title: "선택된 내용이 없습니다." }); + return; + } + if (selected.length > 1) { + Swal.fire({ icon: "warning", title: "한번에 1개의 내용만 등록 가능합니다." }); + return; + } + const contractObjid = String(selected[0].OBJID || ""); + const w = 500; + const h = 350; + const left = (window.screen.width - w) / 2; + const top = (window.screen.height - h) / 2; + window.open( + `/cost/goal/form?contractObjid=${encodeURIComponent(contractObjid)}`, + "costGoalForm", + `width=${w},height=${h},left=${left},top=${top},scrollbars=yes,resizable=yes` + ); + }; + + return ( +
+
+

투입원가관리 현황

+
+ + +
+
+ + + + + + + setProjectNo(e.target.value)} className="w-[140px]" /> + + + + + + + + + + + + + +
+ ); +} diff --git a/src/app/(main)/cs/chart/page.tsx b/src/app/(main)/cs/chart/page.tsx new file mode 100644 index 0000000..a65c97a --- /dev/null +++ b/src/app/(main)/cs/chart/page.tsx @@ -0,0 +1,70 @@ +"use client"; + +import { useState, useCallback, useEffect } from "react"; +import { SearchForm, SearchField } from "@/components/layout/search-form"; +import { Button } from "@/components/ui/button"; +import { CodeSelect } from "@/components/ui/code-select"; + +// csMgmt/csChartList.jsp 대응 - CS 차트관리 +export default function CsChartPage() { + const [year, setYear] = useState(new Date().getFullYear().toString()); + const [productCode, setProductCode] = useState(""); + const [chartData, setChartData] = useState[]>([]); + + const fetchData = useCallback(async () => { + const res = await fetch("/api/cs/chart", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + year, + product_code: productCode, + }), + }); + if (res.ok) { + const json = await res.json(); + setChartData(json.RESULTLIST || []); + } + }, [year, productCode]); + + // 페이지 진입 시 자동 로드 + useEffect(() => { fetchData(); }, [fetchData]); + + return ( +
+
+

CS 차트관리

+
+ +
+
+ + + + + + + + + + +
+ {chartData.length > 0 ? ( +
+ {/* TODO: Chart rendering - integrate with chart library */} +

차트 데이터 {chartData.length}건 로드됨

+

차트 라이브러리 연동 후 표시됩니다.

+
+ ) : ( +
+ 조회 버튼을 클릭하여 차트 데이터를 로드하세요. +
+ )} +
+
+ ); +} diff --git a/src/app/(main)/cs/manage/page.tsx b/src/app/(main)/cs/manage/page.tsx new file mode 100644 index 0000000..e21f854 --- /dev/null +++ b/src/app/(main)/cs/manage/page.tsx @@ -0,0 +1,277 @@ +"use client"; + +import { useState, useCallback, useEffect } from "react"; +import { DataGrid, type GridColumn } from "@/components/grid/data-grid"; +import { SearchForm, SearchField } from "@/components/layout/search-form"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { SearchableCodeSelect } from "@/components/ui/searchable-code-select"; +import { SearchableSelect } from "@/components/ui/searchable-select"; +import { FolderCell } from "@/components/ui/folder-cell"; +import { ApprovalButton } from "@/components/approval/ApprovalButton"; +import { ApprovalStatusBadge } from "@/components/approval/ApprovalStatusBadge"; +import Swal from "sweetalert2"; + +// asMngList_CS.jsp 대응 - CS등록 및 조치 +export default function CsManagePage() { + const [year, setYear] = useState(new Date().getFullYear().toString()); + const [productCd, setProductCd] = useState(""); + const [projectNo, setProjectNo] = useState(""); + const [warranty, setWarranty] = useState(""); + const [recStartDate, setRecStartDate] = useState(""); + const [recEndDate, setRecEndDate] = useState(""); + const [managerId, setManagerId] = useState(""); + const [actStartDate, setActStartDate] = useState(""); + const [actEndDate, setActEndDate] = useState(""); + const [apprStatus, setApprStatus] = useState(""); + + const [projectOpts, setProjectOpts] = useState<{ value: string; label: string }[]>([]); + const [userOpts, setUserOpts] = useState<{ value: string; label: string }[]>([]); + const [data, setData] = useState[]>([]); + const [selectedRows, setSelectedRows] = useState[]>([]); + const [loading, setLoading] = useState(false); + + useEffect(() => { + fetch("/api/common/project-list", { method: "POST", headers: { "Content-Type": "application/json" }, body: "{}" }) + .then((r) => r.json()) + .then((j) => + setProjectOpts( + (j.RESULTLIST || []).map((r: Record) => ({ + value: String(r.OBJID), + label: String(r.LABEL || r.PROJECT_NO || r.OBJID), + })) + ) + ) + .catch(() => setProjectOpts([])); + fetch("/api/admin/users", { method: "POST", headers: { "Content-Type": "application/json" }, body: "{}" }) + .then((r) => r.json()) + .then((j) => + setUserOpts( + (j.RESULTLIST || []).map((u: Record) => ({ + value: String(u.USER_ID), + label: `${u.USER_NAME || u.USER_ID}${u.DEPT_NAME ? ` (${u.DEPT_NAME})` : ""}`, + })) + ) + ) + .catch(() => setUserOpts([])); + }, []); + + const openFormPopup = (objId = "") => { + const w = 1400, h = 930; + const left = (window.screen.width - w) / 2; + const top = (window.screen.height - h) / 2; + window.open( + `/cs/manage/form${objId ? `?objId=${objId}` : ""}`, + "asMngFormPopUp", + `width=${w},height=${h},left=${left},top=${top}` + ); + }; + + const openFileRegist = (objId: string) => { + const w = 800, h = 500; + const left = (window.screen.width - w) / 2; + const top = (window.screen.height - h) / 2; + window.open( + `/common/files?targetObjId=${encodeURIComponent(objId)}&docType=AS_DOC_01&docTypeName=${encodeURIComponent("CS 조치내역 첨부파일")}`, + "fileAS_DOC_01", + `width=${w},height=${h},left=${left},top=${top}` + ); + }; + + const openApprovalDetail = (approvalObjId: string) => { + if (!approvalObjId) return; + const w = 900, h = 700; + const left = (window.screen.width - w) / 2; + const top = (window.screen.height - h) / 2; + window.open( + `/approval/form?objId=${approvalObjId}`, + "approvalDetailPopup", + `width=${w},height=${h},left=${left},top=${top}` + ); + }; + + const columns: GridColumn[] = [ + { + title: "접수 No.", field: "SERVICE_NO", width: 110, hozAlign: "left", + cellClick: (row) => openFormPopup(String(row.OBJID || "")), + }, + { title: "제품구분(기계형식)", field: "PRODUCT_NAME", width: 140, hozAlign: "left" }, + { title: "프로젝트번호", field: "PROJECT_NO", width: 120, hozAlign: "left" }, + { title: "고객사", field: "CUSTOMER_NAME", width: 130, hozAlign: "left" }, + { title: "출고일자", field: "RELEASE_DATE", width: 100, hozAlign: "center" }, + { title: "셋업지", field: "SETUP", width: 120, hozAlign: "left" }, + { title: "유무상", field: "WARRANTY_NAME", width: 80, hozAlign: "left" }, + { title: "CS구분", field: "CATEGORY_NAME", width: 100, hozAlign: "left" }, + { title: "유형", field: "CATEGORY_H_NAME", width: 100, hozAlign: "left" }, + { title: "제목", field: "TITLE", width: 200, hozAlign: "left" }, + { title: "접수일", field: "REC_DT", width: 100, hozAlign: "center" }, + { title: "예상발생비용", field: "PLAN_COST", width: 110, hozAlign: "right", formatter: "money" }, + { title: "등록자", field: "MANAGER_NAME", width: 90, hozAlign: "left" }, + { title: "조치완료일", field: "ACT_DATE", width: 110, hozAlign: "center" }, + { + title: "첨부파일", field: "CU03_CNT", width: 80, hozAlign: "center", + formatter: (cell, row) => ( + openFileRegist(String(row.OBJID || ""))} + /> + ), + }, + { + title: "상태", field: "APPR_STATUS_NAME", width: 100, hozAlign: "center", + formatter: (_, row) => { + const apv = String(row.APPROVAL_OBJID || ""); + return ( + openApprovalDetail(apv) : undefined} + /> + ); + }, + }, + ]; + + const fetchData = useCallback(async () => { + setLoading(true); + try { + const res = await fetch("/api/cs/manage", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + Year: year, + product_cd: productCd, + project_no: projectNo, + warranty, + rec_start_date: recStartDate, + rec_end_date: recEndDate, + manager_id: managerId, + act_start_date: actStartDate, + act_end_date: actEndDate, + appr_status: apprStatus, + }), + }); + if (res.ok) { + const json = await res.json(); + setData(json.RESULTLIST || []); + } + } finally { + setLoading(false); + } + }, [year, productCd, projectNo, warranty, recStartDate, recEndDate, managerId, actStartDate, actEndDate, apprStatus]); + + useEffect(() => { fetchData(); }, [fetchData]); + + const handleDelete = async () => { + if (selectedRows.length === 0) { + Swal.fire("알림", "삭제할 항목을 선택하세요.", "warning"); + return; + } + const result = await Swal.fire({ + title: "삭제 확인", + text: `선택한 ${selectedRows.length}건을 삭제하시겠습니까?`, + icon: "question", showCancelButton: true, + confirmButtonText: "삭제", cancelButtonText: "취소", + }); + if (!result.isConfirmed) return; + const res = await fetch("/api/cs/manage/delete", { + method: "POST", headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ objIds: selectedRows.map((r) => String(r.OBJID)) }), + }); + const json = await res.json(); + if (json.success) { + Swal.fire({ icon: "success", title: json.message || "삭제되었습니다.", timer: 1200, showConfirmButton: false }); + fetchData(); + } else { + Swal.fire("오류", json.message || "삭제 실패", "error"); + } + }; + + // 단건만 결재 허용 + 이미 진행/완료건 필터 + const approvalRow = selectedRows.length === 1 ? selectedRows[0] : null; + const approvalStatus = String(approvalRow?.APPR_STATUS_NAME || ""); + const canRequestApproval = + !!approvalRow && approvalStatus !== "결재중" && approvalStatus !== "결재완료"; + + return ( +
+
+

CS관리_CS등록 및 조회

+
+ + + + +
+
+ + + + + + + + + + + + + + + +
+ setRecStartDate(e.target.value)} className="w-[140px]" /> + ~ + setRecEndDate(e.target.value)} className="w-[140px]" /> +
+
+ + + + +
+ setActStartDate(e.target.value)} className="w-[140px]" /> + ~ + setActEndDate(e.target.value)} className="w-[140px]" /> +
+
+ + + +
+ + +
+ ); +} diff --git a/src/app/(main)/cs/page.tsx b/src/app/(main)/cs/page.tsx new file mode 100644 index 0000000..95e7fa0 --- /dev/null +++ b/src/app/(main)/cs/page.tsx @@ -0,0 +1,68 @@ +"use client"; + +import { useState, useCallback, useEffect } from "react"; +import { DataGrid, type GridColumn } from "@/components/grid/data-grid"; +import { SearchForm, SearchField } from "@/components/layout/search-form"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; + +// customerMng/customerServiceList.jsp 대응 - CS관리 +export default function CsPage() { + const [year, setYear] = useState(new Date().getFullYear().toString()); + const [customerName, setCustomerName] = useState(""); + const [data, setData] = useState[]>([]); + + const columns: GridColumn[] = [ + { title: "접수번호", field: "CS_NO", width: 120, + cellClick: (row) => window.open(`/cs/manage/form?objId=${row.OBJID}`, "csDetail", "width=900,height=700") }, + { title: "고객사", field: "CUSTOMER_NAME", width: 150, hozAlign: "left" }, + { title: "제품명", field: "PRODUCT_NAME", width: 150, hozAlign: "left" }, + { title: "접수일", field: "RECEIPT_DATE", width: 100, hozAlign: "center" }, + { title: "유형", field: "CS_TYPE_NAME", width: 100, hozAlign: "center" }, + { title: "내용", field: "DESCRIPTION", hozAlign: "left" }, + { title: "상태", field: "STATUS_NAME", width: 80, hozAlign: "center" }, + { title: "담당자", field: "CHARGER_NAME", width: 90, hozAlign: "center" }, + { title: "완료일", field: "COMPLETE_DATE", width: 100, hozAlign: "center" }, + ]; + + const fetchData = useCallback(async () => { + const res = await fetch("/api/cs", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ year, customer_name: customerName }), + }); + if (res.ok) { + const json = await res.json(); + setData(json.RESULTLIST || []); + } + }, [year, customerName]); + + // 페이지 진입 시 자동 로드 + useEffect(() => { fetchData(); }, [fetchData]); + + return ( +
+
+

CS 관리

+
+ + +
+
+ + + + + + setCustomerName(e.target.value)} className="w-[150px]" /> + + + +
+ ); +} diff --git a/src/app/(main)/cs/status/page.tsx b/src/app/(main)/cs/status/page.tsx new file mode 100644 index 0000000..26a7e48 --- /dev/null +++ b/src/app/(main)/cs/status/page.tsx @@ -0,0 +1,194 @@ +"use client"; + +import { useState, useCallback, useEffect, useMemo } from "react"; +import { DataGrid, type GridColumn } from "@/components/grid/data-grid"; +import { SearchForm, SearchField } from "@/components/layout/search-form"; +import { Button } from "@/components/ui/button"; +import { SearchableCodeSelect } from "@/components/ui/searchable-code-select"; +import { SearchableSelect } from "@/components/ui/searchable-select"; + +interface DynColumn { + GROUP_SEQ: number; + GROUP_CNT: number; + GROUP_NAME: string; + PARENT_CODE_ID: string; + CODE_ID: string; + NAME: string; + COL_NAME: string; +} + +// asList_CS.jsp 대응 - CS관리_현황 (제품×프로젝트 대시보드) +export default function CsStatusPage() { + const [year, setYear] = useState(new Date().getFullYear().toString()); + const [productCd, setProductCd] = useState(""); + const [projectNo, setProjectNo] = useState(""); + const [warranty, setWarranty] = useState(""); + const [csCategory, setCsCategory] = useState(""); + const [categoryH, setCategoryH] = useState(""); + + const [projectOpts, setProjectOpts] = useState<{ value: string; label: string }[]>([]); + const [categoryHOpts, setCategoryHOpts] = useState<{ value: string; label: string }[]>([]); + const [data, setData] = useState[]>([]); + const [dynColumns, setDynColumns] = useState([]); + const [loading, setLoading] = useState(false); + + useEffect(() => { + fetch("/api/common/project-list", { method: "POST", headers: { "Content-Type": "application/json" }, body: "{}" }) + .then((r) => r.json()) + .then((j) => + setProjectOpts( + (j.RESULTLIST || []).map((r: Record) => ({ + value: String(r.OBJID), + label: String(r.LABEL || r.PROJECT_NO || r.OBJID), + })) + ) + ) + .catch(() => setProjectOpts([])); + }, []); + + // cs_category(0000970) 선택 시 유형(category_h) 옵션 로드 + useEffect(() => { + if (!csCategory) { + setCategoryHOpts([]); + setCategoryH(""); + return; + } + fetch("/api/common/code-list", { + method: "POST", headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ codeId: csCategory }), + }) + .then((r) => r.json()) + .then((j) => + setCategoryHOpts( + (j.data || []).map((r: Record) => ({ + value: String(r.code_id || r.CODE_ID), + label: String(r.code_name || r.CODE_NAME), + })) + ) + ) + .catch(() => setCategoryHOpts([])); + }, [csCategory]); + + const fetchData = useCallback(async () => { + setLoading(true); + try { + const res = await fetch("/api/cs/status", { + method: "POST", headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + Year: year, + product_cd: productCd, + project_no: projectNo, + warranty, + cs_category: csCategory, + category_h: categoryH, + }), + }); + if (res.ok) { + const json = await res.json(); + setData(json.RESULTLIST || []); + setDynColumns(json.COLUMN_LIST || []); + } + } finally { + setLoading(false); + } + }, [year, productCd, projectNo, warranty, csCategory, categoryH]); + + useEffect(() => { fetchData(); }, [fetchData]); + + const columns: GridColumn[] = useMemo(() => { + // 동적 컬럼 → GROUP_NAME(2nd-level 부모코드명) 기준으로 묶고, leaf는 NAME 사용 + const groupMap = new Map(); + const groupOrder: string[] = []; + dynColumns.forEach((c) => { + const key = `${c.PARENT_CODE_ID}::${c.GROUP_NAME}`; + if (!groupMap.has(key)) { + groupMap.set(key, []); + groupOrder.push(key); + } + groupMap.get(key)!.push(c); + }); + + const dynGroupColumns: GridColumn[] = groupOrder.map((key) => { + const group = groupMap.get(key)!; + return { + title: group[0].GROUP_NAME || "유형", + columns: group.map((c) => ({ + title: c.NAME, + field: c.COL_NAME, + width: 90, + hozAlign: "right", + headerHozAlign: "center", + formatter: "money", + })), + }; + }); + + // 유형 최상위 묶음 (있을 때만) + const categoryWrapper: GridColumn[] = dynGroupColumns.length + ? [{ title: "유형", columns: dynGroupColumns }] + : []; + + return [ + { title: "제품구분", field: "PRODUCT_NAME", width: 140, hozAlign: "left" }, + { title: "프로젝트번호", field: "PROJECT_NO", width: 130, hozAlign: "left" }, + { + title: "유상", + columns: [ + { title: "건수", field: "WARRANTY1", width: 80, hozAlign: "right", headerHozAlign: "center", formatter: "money" }, + { title: "발생비용", field: "COST1", width: 110, hozAlign: "right", headerHozAlign: "center", formatter: "money" }, + ], + }, + { + title: "무상", + columns: [ + { title: "건수", field: "WARRANTY2", width: 80, hozAlign: "right", headerHozAlign: "center", formatter: "money" }, + { title: "발생비용", field: "COST2", width: 110, hozAlign: "right", headerHozAlign: "center", formatter: "money" }, + ], + }, + ...categoryWrapper, + ]; + }, [dynColumns]); + + return ( +
+
+

CS관리_현황

+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + +
+ ); +} diff --git a/src/app/(main)/dashboard/page.tsx b/src/app/(main)/dashboard/page.tsx new file mode 100644 index 0000000..245f471 --- /dev/null +++ b/src/app/(main)/dashboard/page.tsx @@ -0,0 +1,576 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { + PieChart, Pie, Cell, Tooltip, ResponsiveContainer, Legend, + ComposedChart, Bar, Line, XAxis, YAxis, CartesianGrid, +} from "recharts"; +import { useAuthStore } from "@/store/auth-store"; +import { useMenuStore } from "@/store/menu-store"; +import { numberWithCommas } from "@/lib/utils"; +import { Button } from "@/components/ui/button"; +import { + FolderKanban, AlertCircle, + TrendingUp, BarChart3, Briefcase, +} from "lucide-react"; + +interface YearGoalRow { + YEAR: string; + CONTRACT_CNT_YEAR_IN?: number; + CONTRACT_CNT_YEAR_OUT?: number; + CONTRACT_CNT_YEAR_RATE?: number; + CONTRACT_COST_YEAR?: string | number; + PRICE?: string | number; + GOAL_RATE?: number; + YEAR_GOAL_OBJID?: string; +} + +interface DashboardData { + projectStats: { + CNT_TOTAL?: number; CNT_NOPLAN?: number; CNT_ING?: number; + CNT_DELAY?: number; CNT_END?: number; + ISSUE_TOTAL?: number; ISSUE_MISS?: number; + }; + productDist: { CODE: string; NAME: string; CNT: number }[]; + supplyDist: { CODE: string; NAME: string; CNT: number }[]; + monthlyContract: { MONTH: number; AMOUNT: string }[]; + projectList: Record[]; + yearGoalInfo: YearGoalRow[]; +} + +type Tab = "sales" | "project"; + +const PIE_COLORS = ["#3b82f6", "#22c55e", "#a855f7", "#f97316", "#ef4444", "#14b8a6", "#eab308"]; + +export default function DashboardPage() { + const { user } = useAuthStore(); + const { topMenus, fetchSideMenus } = useMenuStore(); + const [data, setData] = useState(null); + const [year, setYear] = useState(new Date().getFullYear().toString()); + const [tab, setTab] = useState("sales"); + + useEffect(() => { + if (topMenus.length > 0) { + const userMenu = topMenus.find((m) => m.MENU_NAME_KOR !== "관리자") || topMenus[0]; + fetchSideMenus(userMenu.OBJID); + } + }, [topMenus, fetchSideMenus]); + + useEffect(() => { + fetch("/api/dashboard", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ year }), + }) + .then((r) => r.ok ? r.json() : null) + .then((d) => setData(d)) + .catch(() => {}); + }, [year]); + + return ( +
+ {/* 헤더 */} +
+

+ Dashboard + + {user?.userName}님, 환영합니다. + +

+
+ {/* 탭 */} +
+ setTab("sales")} icon={TrendingUp} label="영업" /> + setTab("project")} icon={Briefcase} label="프로젝트" /> +
+ +
+
+ + {/* 탭 내용 — 남은 공간 가득 채움 */} +
+ {tab === "sales" ? ( + + ) : ( + + )} +
+
+ ); +} + +function TabButton({ active, onClick, icon: Icon, label }: { + active: boolean; onClick: () => void; icon: React.ElementType; label: string; +}) { + return ( + + ); +} + +function SalesTab({ data, year }: { data: DashboardData | null; year: string }) { + const openGoalPopup = () => { + const w = 700, h = 500; + const left = (window.screen.width - w) / 2; + const top = (window.screen.height - h) / 2; + window.open(`/sales/year-goal?year=${year}`, "yearGoalPopup", + `width=${w},height=${h},left=${left},top=${top},menubars=no,scrollbars=yes,resizable=yes`); + }; + + return ( +
+ {/* 상단: 영업현황 표 (고정 높이) */} + + + {/* 하단: 3분할 — 제품별 pie / 고객사별 pie / 년도별 combo (남은 공간 가득) */} +
+ + + +
+
+ ); +} + +function YearGoalTable({ info, onOpenGoal }: { info: YearGoalRow[]; onOpenGoal: () => void }) { + return ( +
+
+

+ + 영업현황 +

+ +
+ + + + + + + + + + + + + + + + + {info.length === 0 ? ( + + ) : info.map((row, idx) => ( + + + + + + + + + + ))} + +
년도수주현황(건수)수주율(%)예상매출(억원)영업목표(억원)달성율(%)
국내해외
데이터가 없습니다.
{row.YEAR}{row.CONTRACT_CNT_YEAR_IN ?? 0}{row.CONTRACT_CNT_YEAR_OUT ?? 0}{row.CONTRACT_CNT_YEAR_RATE ?? 0}{numberWithCommas(Number(row.CONTRACT_COST_YEAR ?? 0))}{numberWithCommas(Number(row.PRICE ?? 0))}{row.GOAL_RATE ?? 0}
+
+ ); +} + +function PieCard({ title, data }: { title: string; data: { CODE: string; NAME: string; CNT: number }[] }) { + const chartData = data.map((d, i) => ({ + name: d.NAME || `코드 ${d.CODE}`, + value: d.CNT, + color: PIE_COLORS[i % PIE_COLORS.length], + })); + const total = chartData.reduce((s, d) => s + d.value, 0); + + return ( +
+

{title}

+ {total === 0 ? ( +
데이터가 없습니다.
+ ) : ( +
+ + + + percent != null && percent >= 0.05 ? `${Math.round(percent * 100)}%` : "" + } + labelLine={false} + > + {chartData.map((entry, index) => ( + + ))} + + `${v}건`} /> + + + +
+ )} +
+ ); +} + +function YearSalesComboChart({ info }: { info: YearGoalRow[] }) { + // 과거→현재 순서 + const chartData = [...info] + .sort((a, b) => Number(a.YEAR) - Number(b.YEAR)) + .map((r) => ({ + YEAR: r.YEAR, + 영업목표: Number(r.PRICE || 0), + 수주금액: Number(r.CONTRACT_COST_YEAR || 0), + 달성율: Number(r.GOAL_RATE || 0), + })); + + return ( +
+

■ 년도별 영업현황

+
+ + + + + + + + name === "달성율" ? `${v}%` : `${numberWithCommas(Number(v))}억` + } + /> + + + + + + +
+
+ ); +} + +type StatusFilter = "all" | "noplan" | "ing" | "delay" | "end"; + +const FILTER_LABELS: Record = { + all: "전체", + noplan: "계획미수립", + ing: "진행중", + delay: "지연", + end: "종료", +}; + +function ProjectTab({ data, year }: { data: DashboardData | null; year: string }) { + const stats = data?.projectStats || {}; + const allProjects = (data?.projectList || []) as Record[]; + const [statusFilter, setStatusFilter] = useState("all"); + const [projectFilter, setProjectFilter] = useState(""); + const [selectedIdx, setSelectedIdx] = useState(0); + + const projectList = allProjects.filter((p) => { + const s = String(p.STATUS_TITLE || ""); + if (statusFilter !== "all") { + if (statusFilter === "noplan" && s !== "계획미수립") return false; + if (statusFilter === "ing" && s !== "진행중") return false; + if (statusFilter === "delay" && s !== "지연") return false; + if (statusFilter === "end" && s !== "종료") return false; + } + if (projectFilter && String(p.OBJID) !== projectFilter) return false; + return true; + }); + const selected = projectList[selectedIdx] || projectList[0]; + + const toggleFilter = (f: StatusFilter) => { + setSelectedIdx(0); + setStatusFilter((cur) => (cur === f ? "all" : f)); + }; + + const openProjectSchedule = () => { + // 프로젝트 일정 전체 보기 → 프로젝트 관리 > 종합현황 페이지로 이동 + window.location.href = `/project/total?year=${year}`; + }; + + return ( +
+ {/* 상단 프로젝트현황 카드 — 원본 스타일 (5개 숫자 가로 + 컨트롤) */} +
+
+

+ + 프로젝트현황 +

+ +
+
+ {/* 좌측: 년도/프로젝트번호 셀렉트 */} +
+
+ +
{year}
+
+
+ + +
+
+ {/* 우측: 5개 숫자 가로 배치 */} +
+ setStatusFilter("all")} /> + toggleFilter("noplan")} /> + toggleFilter("ing")} /> + toggleFilter("delay")} /> + toggleFilter("end")} /> +
+
+
+ + {/* 프로젝트 리스트 — 전체 너비, 원본 10컬럼 구조 */} +
+

+ + 프로젝트 리스트 {statusFilter !== "all" && ( + [{FILTER_LABELS[statusFilter]}] + )} · 총 {projectList.length}건 + {statusFilter !== "all" && ( + + )} +

+
+ + + + + + + + + + + + + + + + + {projectList.length === 0 ? ( + + ) : projectList.map((pjt, idx) => { + const statusTitle = String(pjt.STATUS_TITLE || ""); + const statusColor = + statusTitle === "종료" ? "text-green-600" : + statusTitle === "지연" ? "text-red-500" : + statusTitle === "계획미수립" ? "text-gray-500" : + "text-blue-600"; + const isSelected = idx === selectedIdx; + return ( + setSelectedIdx(idx)}> + + + + + + + + + + + + ); + })} + +
선택고객사제품구분프로젝트번호납기일셋업지제작공장진척율(%)셋업완료일상태
데이터가 없습니다.
+ setSelectedIdx(idx)} className="pointer-events-none" /> + {String(pjt.CUSTOMER_NAME || "")}{String(pjt.PRODUCT_NAME || "")} + {String(pjt.PROJECT_NO || "")} + {String(pjt.CONTRACT_DEL_DATE || "-")}{String(pjt.SETUP || "")}{String(pjt.MANUFACTURE_PLANT_NAME || "")}{Number(pjt.SETUP_RATE || 0).toFixed(1)}{String(pjt.SETUP_DONE_DATE || "")}{statusTitle}
+
+
+ + {/* 선택된 프로젝트 상세 (이슈 + 투입원가) */} +
+ +
+
+ ); +} + +function ProjectDetailPanel({ project }: { project: Record | undefined }) { + if (!project) { + return ( +
+
프로젝트를 선택하세요
+
+ ); + } + + const issueTotal = Number(project.ISSUE_CNT || 0); + const issueDone = Number(project.ISSUE_DONE_CNT || 0); + const issueMiss = Number(project.ISSUE_MISS_CNT || 0); + const issueRate = issueTotal > 0 ? Math.round((issueDone / issueTotal) * 100) : 0; + + // 투입원가 항목별 (원본 dashboard.jsp fn_getProjectCostStatusList 이식) + const contractPrice = Number(project.CONTRACT_PRICE || 0); + const materialGoal = Number(project.MATERIAL_COST_GOAL || 0); + const materialActual = Number(project.ACCRUAL_MATERIAL_COST || 0); + const laborGoal = Number(project.LABOR_COST_GOAL || 0); + const laborActual = Number(project.LABOR_COST_ACTUAL || 0); + const expenseGoal = Number(project.EXPENSE_COST_GOAL || 0); + const expenseActual = Number(project.ACCRUAL_EXPENSE || 0); + const totalGoalBase = materialGoal + laborGoal + expenseGoal; + const totalActualBase = materialActual + laborActual + expenseActual; + // 관리비 = 전체의 10% + const mgmtGoal = Math.round(totalGoalBase * 0.1); + const mgmtActual = Math.round(totalActualBase * 0.1); + const totalGoal = totalGoalBase + mgmtGoal; + const totalActual = totalActualBase + mgmtActual; + // 각 항목 투입율(%) — 재료비는 수주가 기준, 나머지는 목표 기준 (원본 로직) + const materialRate = contractPrice > 0 ? Math.round((materialActual / contractPrice) * 1000) / 10 : 0; + const laborRate = laborGoal > 0 ? Math.round((laborActual / laborGoal) * 1000) / 10 : 0; + const expenseRate = expenseGoal > 0 ? Math.round((expenseActual / expenseGoal) * 1000) / 10 : 0; + const mgmtRate = mgmtGoal > 0 ? Math.round((mgmtActual / mgmtGoal) * 1000) / 10 : 0; + const totalRateCost = totalGoal > 0 ? Math.round((totalActual / totalGoal) * 1000) / 10 : 0; + + return ( + <> + {/* 이슈 + 투입원가 2분할 (가로 배치) */} +
+ {/* 이슈 (Quality) */} +
+

+ + 이슈 (Quality) +

+
+ + + + +
+
+ + {/* 투입원가현황 — 원본 dashboard.jsp 5행 테이블 */} +
+

+ + 투입원가현황 +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
수주가(원)항목목표원가(원)투입원가(원)투입율(%)
{numberWithCommas(contractPrice)}재료비{numberWithCommas(materialGoal)}{numberWithCommas(materialActual)}{materialRate}
노무비{numberWithCommas(laborGoal)}{numberWithCommas(laborActual)}{laborRate}
경비{numberWithCommas(expenseGoal)}{numberWithCommas(expenseActual)}{expenseRate}
관리비{numberWithCommas(mgmtGoal)}{numberWithCommas(mgmtActual)}{mgmtRate}
{numberWithCommas(totalGoal)}{numberWithCommas(totalActual)}{totalRateCost}
+
+
+
+ + ); +} + +function MiniStat({ label, value, color }: { label: string; value: number | string; color: string }) { + return ( +
+
{label}
+
{value}
+
+ ); +} + +function CountBadge({ label, value, color, active, onClick }: { + label: string; value: number; color: "blue" | "red"; + active?: boolean; onClick?: () => void; +}) { + const numColor = value > 0 ? (color === "red" ? "text-red-500" : "text-blue-600") : "text-gray-300"; + const bg = active ? (color === "red" ? "bg-red-50" : "bg-blue-50") : "hover:bg-gray-50"; + return ( + + ); +} + diff --git a/src/app/(main)/delivery/acceptance/page.tsx b/src/app/(main)/delivery/acceptance/page.tsx new file mode 100644 index 0000000..2bfaf03 --- /dev/null +++ b/src/app/(main)/delivery/acceptance/page.tsx @@ -0,0 +1,352 @@ +"use client"; + +import { useState, useCallback, useEffect } from "react"; +import { DataGrid, type GridColumn } from "@/components/grid/data-grid"; +import { SearchForm, SearchField } from "@/components/layout/search-form"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { SearchableSelect } from "@/components/ui/searchable-select"; +import { SearchableCodeSelect } from "@/components/ui/searchable-code-select"; +import Swal from "sweetalert2"; + +type Option = { value: string; label: string }; + +// 원본: purchaseOrder/deliveryMngAcceptanceList.jsp +// 입고관리 > 입고결과등록 +export default function AcceptancePage() { + const currentYear = new Date().getFullYear(); + const [year, setYear] = useState(""); + const [customerProjectName, setCustomerProjectName] = useState(""); + const [projectNo, setProjectNo] = useState(""); + const [unitCode, setUnitCode] = useState(""); + const [purchaseOrderNo, setPurchaseOrderNo] = useState(""); + const [type, setType] = useState(""); + const [searchPartSpec, setSearchPartSpec] = useState(""); + const [partnerObjid, setPartnerObjid] = useState(""); + const [salesMngUserId, setSalesMngUserId] = useState(""); + const [deliveryStartDate, setDeliveryStartDate] = useState(""); + const [deliveryEndDate, setDeliveryEndDate] = useState(""); + const [regStartDate, setRegStartDate] = useState(""); + const [regEndDate, setRegEndDate] = useState(""); + const [deliveryStatus, setDeliveryStatus] = useState(""); + const [searchPartName, setSearchPartName] = useState(""); + const [searchPartNo, setSearchPartNo] = useState(""); + const [poClientId, setPoClientId] = useState(""); + + const [supplyOptions, setSupplyOptions] = useState([]); + const [userOptions, setUserOptions] = useState([]); + + const [data, setData] = useState[]>([]); + const [selectedRows, setSelectedRows] = useState[]>([]); + + useEffect(() => { + fetch("/api/admin/supply", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: "{}", + }) + .then((r) => r.json()) + .then((d) => + setSupplyOptions( + (d.RESULTLIST || []).map((r: Record) => ({ + value: String(r.OBJID), + label: String(r.SUPPLY_NAME), + })), + ), + ) + .catch(() => {}); + + fetch("/api/admin/users", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: "{}", + }) + .then((r) => r.json()) + .then((d) => + setUserOptions( + (d.RESULTLIST || []).map((r: Record) => ({ + value: String(r.USER_ID), + label: String(r.USER_NAME), + })), + ), + ) + .catch(() => {}); + }, []); + + // 발주서 상세 팝업 열기 (발주번호 셀 클릭) + const openOrderForm = (objId: string) => { + const w = 1460; + const h = 1050; + const left = (window.screen.width - w) / 2; + const top = (window.screen.height - h) / 2; + window.open( + `/order/list/form?objId=${objId}&action=view`, + `orderForm_${objId}`, + `width=${w},height=${h},left=${left},top=${top}`, + ); + }; + + // 입고결과 팝업 (view) — 입고결과 셀 클릭 + const openAcceptanceViewPopup = (objId: string, status: string) => { + const w = 1560; + const h = 1050; + const left = (window.screen.width - w) / 2; + const top = (window.screen.height - h) / 2; + window.open( + `/delivery/acceptance/form?objId=${objId}&delivery_status=${encodeURIComponent(status)}`, + "deliveryAcceptancePopUp", + `width=${w},height=${h},left=${left},top=${top}`, + ); + }; + + const columns: GridColumn[] = [ + { title: "고객사", field: "CUSTOMER_NAME", width: 110, hozAlign: "left" }, + { title: "프로젝트명", field: "CUSTOMER_PROJECT_NAME", width: 150, hozAlign: "left" }, + { title: "유닛명", field: "UNIT_NAME", width: 200, hozAlign: "left" }, + { title: "프로젝트번호", field: "PROJECT_NO", width: 100, hozAlign: "center" }, + { + title: "발주번호", + field: "PURCHASE_ORDER_NO", + width: 100, + hozAlign: "center", + cellClick: (row) => openOrderForm(String(row.OBJID || "")), + }, + { title: "동시", field: "MULTI_YN_MAKED", width: 50, hozAlign: "center" }, + { title: "발주서_제목", field: "TITLE", width: 150, hozAlign: "left" }, + { title: "입고요청일", field: "DELIVERY_DATE", width: 85, hozAlign: "center" }, + { title: "구매/제작업체명", field: "PARTNER_NAME", width: 120, hozAlign: "left" }, + { title: "구매담당", field: "SALES_MNG_USER_NAME", width: 78, hozAlign: "center" }, + { title: "발주일", field: "REGDATE", width: 78, hozAlign: "center" }, + { title: "발주수량", field: "TOTAL_PO_QTY", width: 78, hozAlign: "right", formatter: "money" }, + { title: "입고일", field: "CUR_DELIVERY_DATE", width: 78, hozAlign: "center" }, + { title: "입고자", field: "CUR_RECEIVER_NAME", width: 70, hozAlign: "center" }, + { title: "입고수량", field: "TOTAL_DELIVERY_QTY", width: 75, hozAlign: "right", formatter: "money" }, + { title: "미입고수량", field: "NON_DELIVERY_QTY", width: 85, hozAlign: "right", formatter: "money" }, + { + title: "입고결과", + field: "DELIVERY_STATUS", + width: 75, + hozAlign: "center", + cellClick: (row) => + openAcceptanceViewPopup(String(row.OBJID || ""), String(row.DELIVERY_STATUS || "")), + }, + ]; + + const fetchData = useCallback(async () => { + const res = await fetch("/api/delivery/acceptance", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + year, + customer_project_name: customerProjectName, + project_nos: projectNo ? [projectNo] : [], + unit_code: unitCode, + purchase_order_no: purchaseOrderNo, + type, + SEARCH_PART_SPEC: searchPartSpec, + partner_objid: partnerObjid, + sales_mng_user_ids: salesMngUserId ? [salesMngUserId] : [], + delivery_start_date: deliveryStartDate, + delivery_end_date: deliveryEndDate, + reg_start_date: regStartDate, + reg_end_date: regEndDate, + delivery_status: deliveryStatus, + SEARCH_PART_NAME: searchPartName, + SEARCH_PART_NO: searchPartNo, + po_client_id: poClientId, + }), + }); + if (res.ok) { + const json = await res.json(); + setData(json.RESULTLIST || []); + } else { + const j = await res.json().catch(() => ({})); + Swal.fire("오류", j.message || "조회 실패", "error"); + } + }, [ + year, customerProjectName, projectNo, unitCode, purchaseOrderNo, type, + searchPartSpec, partnerObjid, salesMngUserId, deliveryStartDate, deliveryEndDate, + regStartDate, regEndDate, deliveryStatus, searchPartName, searchPartNo, poClientId, + ]); + + // 입고결과등록: 원본 가드 로직 동일 + // - 미선택: "선택된 데이터가 없습니다." + // - 2건이상: "한건씩 등록 가능합니다." + // - MULTI_YN='Y' AND MULTI_MASTER_YN='N': "동시발주건은 마스터건으로 수입검사해야 합니다." + const handleAcceptanceRegister = () => { + if (selectedRows.length === 0) { + Swal.fire("알림", "선택된 데이터가 없습니다.", "warning"); + return; + } + if (selectedRows.length > 1) { + Swal.fire("알림", "한건씩 등록 가능합니다.", "warning"); + return; + } + const row = selectedRows[0]; + const multiYn = String(row.MULTI_YN || ""); + const multiMasterYn = String(row.MULTI_MASTER_YN || ""); + if (multiYn === "Y" && multiMasterYn === "N") { + Swal.fire("알림", "동시발주건은 마스터건으로 수입검사해야 합니다.", "warning"); + return; + } + const w = 1560; + const h = 1050; + const left = (window.screen.width - w) / 2; + const top = (window.screen.height - h) / 2; + window.open( + `/delivery/acceptance/form?objId=${row.OBJID}&action=regist`, + "deliveryAcceptancePopUp", + `width=${w},height=${h},left=${left},top=${top}`, + ); + }; + + // 부적합등록: 원본 가드 로직 동일 + // - 미선택/복수선택 체크 + MULTI_MASTER_YN='N': "동시발주건은 마스터건으로 부적합 등록해야 합니다." + const handleDefectRegister = () => { + if (selectedRows.length === 0) { + Swal.fire("알림", "선택된 데이터가 없습니다.", "warning"); + return; + } + if (selectedRows.length > 1) { + Swal.fire("알림", "한건씩 등록 가능합니다.", "warning"); + return; + } + const row = selectedRows[0]; + const multiMasterYn = String(row.MULTI_MASTER_YN || ""); + if (multiMasterYn === "N") { + Swal.fire("알림", "동시발주건은 마스터건으로 부적합 등록해야 합니다.", "warning"); + return; + } + const w = 1260; + const h = 1050; + const left = (window.screen.width - w) / 2; + const top = (window.screen.height - h) / 2; + window.open( + `/delivery/defect/form?objId=${row.OBJID}`, + "InvalidFormPopUp", + `width=${w},height=${h},left=${left},top=${top}`, + ); + }; + + useEffect(() => { + fetchData(); + // 최초 1회만 + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return ( +
+
+

입고관리_입고결과등록

+
+ + + +
+
+ + + + + + + setCustomerProjectName(e.target.value)} className="w-[150px]" /> + + + setProjectNo(e.target.value)} className="w-[140px]" placeholder="프로젝트 OBJID" /> + + + setUnitCode(e.target.value)} className="w-[130px]" placeholder="유닛 OBJID" /> + + + setPurchaseOrderNo(e.target.value)} className="w-[120px]" /> + + + + + + setSearchPartSpec(e.target.value)} className="w-[120px]" /> + + + + + + + + + +
+ setDeliveryStartDate(e.target.value)} + className="h-9 rounded border border-gray-300 bg-white px-2 text-sm w-[130px]" + /> + ~ + setDeliveryEndDate(e.target.value)} + className="h-9 rounded border border-gray-300 bg-white px-2 text-sm w-[130px]" + /> +
+
+ +
+ setRegStartDate(e.target.value)} + className="h-9 rounded border border-gray-300 bg-white px-2 text-sm w-[130px]" + /> + ~ + setRegEndDate(e.target.value)} + className="h-9 rounded border border-gray-300 bg-white px-2 text-sm w-[130px]" + /> +
+
+ + + + + setSearchPartName(e.target.value)} className="w-[130px]" /> + + + setSearchPartNo(e.target.value)} className="w-[130px]" /> + + + + + +
+ + +
+ ); +} diff --git a/src/app/(main)/delivery/defect/page.tsx b/src/app/(main)/delivery/defect/page.tsx new file mode 100644 index 0000000..d3e4329 --- /dev/null +++ b/src/app/(main)/delivery/defect/page.tsx @@ -0,0 +1,108 @@ +"use client"; + +import { useState, useCallback, useEffect } from "react"; +import { DataGrid, type GridColumn } from "@/components/grid/data-grid"; +import { SearchForm, SearchField } from "@/components/layout/search-form"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { CodeSelect } from "@/components/ui/code-select"; +import Swal from "sweetalert2"; + +// 입고관리 > 부적합리스트 +export default function DefectPage() { + const [year, setYear] = useState(new Date().getFullYear().toString()); + const [customerCd, setCustomerCd] = useState(""); + const [projectNo, setProjectNo] = useState(""); + const [partnerObjid, setPartnerObjid] = useState(""); + const [defectType, setDefectType] = useState(""); + const [data, setData] = useState[]>([]); + const [selectedRows, setSelectedRows] = useState[]>([]); + + const columns: GridColumn[] = [ + { title: "부적합번호", field: "DEFECT_NO", width: 130, hozAlign: "left", + cellClick: (row) => { + window.open(`/delivery/defect/form?objId=${row.OBJID}`, "defectForm", "width=1000,height=800"); + }, + }, + { title: "발주No", field: "PURCHASE_ORDER_NO", width: 140, hozAlign: "left" }, + { title: "고객사", field: "CUSTOMER_NAME", width: 150, hozAlign: "left" }, + { title: "프로젝트번호", field: "PROJECT_NO", width: 130, hozAlign: "left" }, + { title: "공급업체", field: "PARTNER_NAME", width: 150, hozAlign: "left" }, + { title: "Part No", field: "PART_NO", width: 120, hozAlign: "left" }, + { title: "품명", field: "PART_NAME", width: 150, hozAlign: "left" }, + { title: "부적합유형", field: "DEFECT_TYPE_NAME", width: 100, hozAlign: "center" }, + { title: "부적합수량", field: "DEFECT_QTY", width: 90, hozAlign: "right", formatter: "money" }, + { title: "부적합내용", field: "DEFECT_CONTENT", width: 250, hozAlign: "left" }, + { title: "처리상태", field: "STATUS_NAME", width: 80, hozAlign: "center" }, + { title: "등록일", field: "REG_DATE", width: 100, hozAlign: "center" }, + { title: "등록자", field: "REG_USER_NAME", width: 80, hozAlign: "center" }, + ]; + + const fetchData = useCallback(async () => { + const res = await fetch("/api/delivery/defect", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + year, + customer_cd: customerCd, + project_no: projectNo, + partner_objid: partnerObjid, + defect_type: defectType, + }), + }); + if (res.ok) { + const json = await res.json(); + setData(json.RESULTLIST || []); + } + }, [year, customerCd, projectNo, partnerObjid, defectType]); + + const handleExcelDownload = () => { + Swal.fire("알림", "Excel 다운로드 기능은 준비 중입니다.", "info"); + }; + + // 페이지 진입 시 자동 로드 + useEffect(() => { fetchData(); }, [fetchData]); + + return ( +
+
+

부적합리스트

+
+ + +
+
+ + + + + + + + + + setProjectNo(e.target.value)} placeholder="프로젝트번호" className="w-[150px]" /> + + + + + + + + + + +
+ ); +} diff --git a/src/app/(main)/delivery/price/page.tsx b/src/app/(main)/delivery/price/page.tsx new file mode 100644 index 0000000..7a461a3 --- /dev/null +++ b/src/app/(main)/delivery/price/page.tsx @@ -0,0 +1,108 @@ +"use client"; + +import { useState, useCallback, useEffect } from "react"; +import { DataGrid, type GridColumn } from "@/components/grid/data-grid"; +import { SearchForm, SearchField } from "@/components/layout/search-form"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { CodeSelect } from "@/components/ui/code-select"; +import Swal from "sweetalert2"; + +// 입고관리 > 단가관리 +export default function PricePage() { + const [partnerObjid, setPartnerObjid] = useState(""); + const [partName, setPartName] = useState(""); + const [partNo, setPartNo] = useState(""); + const [data, setData] = useState[]>([]); + const [selectedRows, setSelectedRows] = useState[]>([]); + + const columns: GridColumn[] = [ + { title: "공급업체", field: "PARTNER_NAME", width: 180, hozAlign: "left" }, + { title: "Part No", field: "PART_NO", width: 120, hozAlign: "left" }, + { title: "품명", field: "PART_NAME", width: 150, hozAlign: "left" }, + { title: "규격", field: "SPEC", width: 150, hozAlign: "left" }, + { title: "재질", field: "MATERIAL", width: 100, hozAlign: "left" }, + { title: "단위", field: "UNIT_NAME", width: 60, hozAlign: "center" }, + { title: "단가", field: "UNIT_PRICE", width: 100, hozAlign: "right", formatter: "money" }, + { title: "적용시작일", field: "START_DATE", width: 100, hozAlign: "center" }, + { title: "적용종료일", field: "END_DATE", width: 100, hozAlign: "center" }, + { title: "등록일", field: "REG_DATE", width: 100, hozAlign: "center" }, + { title: "비고", field: "REMARK", width: 200, hozAlign: "left" }, + ]; + + const fetchData = useCallback(async () => { + const res = await fetch("/api/delivery/price", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + partner_objid: partnerObjid, + part_name: partName, + part_no: partNo, + }), + }); + if (res.ok) { + const json = await res.json(); + setData(json.RESULTLIST || []); + } + }, [partnerObjid, partName, partNo]); + + const handleDelete = async () => { + if (selectedRows.length === 0) { + Swal.fire("알림", "삭제할 항목을 선택하세요.", "warning"); + return; + } + const result = await Swal.fire({ + title: "삭제 확인", + text: `선택한 ${selectedRows.length}건을 삭제하시겠습니까?`, + icon: "question", + showCancelButton: true, + confirmButtonText: "삭제", + cancelButtonText: "취소", + }); + if (result.isConfirmed) { + Swal.fire("완료", "삭제되었습니다.", "success"); + fetchData(); + } + }; + + const handleExcelDownload = () => { + Swal.fire("알림", "Excel 다운로드 기능은 준비 중입니다.", "info"); + }; + + // 페이지 진입 시 자동 로드 + useEffect(() => { fetchData(); }, [fetchData]); + + return ( +
+
+

단가관리

+
+ + + + +
+
+ + + + + + + setPartNo(e.target.value)} placeholder="Part No" className="w-[150px]" /> + + + setPartName(e.target.value)} placeholder="품명" className="w-[150px]" /> + + + + +
+ ); +} diff --git a/src/app/(main)/delivery/status/page.tsx b/src/app/(main)/delivery/status/page.tsx new file mode 100644 index 0000000..0d2a0a8 --- /dev/null +++ b/src/app/(main)/delivery/status/page.tsx @@ -0,0 +1,196 @@ +"use client"; + +import { useState, useCallback, useEffect } from "react"; +import { DataGrid, type GridColumn } from "@/components/grid/data-grid"; +import { SearchForm, SearchField } from "@/components/layout/search-form"; +import { Button } from "@/components/ui/button"; +import { SearchableSelect } from "@/components/ui/searchable-select"; +import Swal from "sweetalert2"; + +type Option = { value: string; label: string }; + +// 원본: purchaseOrder/deliveryMngStatus.jsp +// 입고관리 > 현황 — 프로젝트 BOM별 발주/입고/부적합 집계 리포트 +export default function DeliveryStatusPage() { + const currentYear = new Date().getFullYear(); + const [year, setYear] = useState(""); + const [customerObjid, setCustomerObjid] = useState(""); + const [projectNo, setProjectNo] = useState(""); // 단일 project objid + const [data, setData] = useState[]>([]); + + const [customerOptions, setCustomerOptions] = useState([]); + const [projectOptions, setProjectOptions] = useState([]); + + useEffect(() => { + fetch("/api/common/supply-list", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: "{}", + }) + .then((r) => r.json()) + .then((d) => + setCustomerOptions( + (d.RESULTLIST || []).map((r: Record) => ({ + value: String(r.OBJID), + label: String(r.SUPPLY_NAME), + })), + ), + ) + .catch(() => {}); + + fetch("/api/common/project-list", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: "{}", + }) + .then((r) => r.json()) + .then((d) => + setProjectOptions( + (d.RESULTLIST || []).map((r: Record) => ({ + value: String(r.OBJID), + label: String(r.LABEL || r.PROJECT_NO || r.CUSTOMER_PROJECT_NAME || r.OBJID), + })), + ), + ) + .catch(() => {}); + }, []); + + // 구매BOM 팝업 + const openBomPopup = (bomReportObjId: string) => { + if (!bomReportObjId) return; + const w = 1600; + const h = 900; + const left = (window.screen.width - w) / 2; + const top = (window.screen.height - h) / 2; + window.open( + `/purchase/bom/form?parentObjId=${bomReportObjId}&actType=view`, + `bomReport_${bomReportObjId}`, + `width=${w},height=${h},left=${left},top=${top}`, + ); + }; + + const columns: GridColumn[] = [ + { + title: "프로젝트정보", + headerHozAlign: "center", + frozen: true, + columns: [ + { title: "고객사", field: "CUSTOMER_NAME", width: 110, hozAlign: "left" }, + { title: "프로젝트명", field: "CUSTOMER_PROJECT_NAME", width: 140, hozAlign: "left" }, + { title: "유닛명", field: "UNIT_PART_NAME", width: 200, hozAlign: "left" }, + { title: "프로젝트번호", field: "PROJECT_NO", width: 100, hozAlign: "left" }, + ], + }, + { + title: "발주내역", + headerHozAlign: "center", + columns: [ + { + title: "구매BOM", + field: "TOTAL_BOM_PART_CNT", + width: 80, + hozAlign: "center", + cellClick: (row) => openBomPopup(String(row.BOM_REPORT_OBJID || "")), + formatter: (cell) => (Number(cell) > 0 ? "조회" : "-"), + }, + { title: "전체품목수", field: "TOTAL_BOM_PART_CNT", width: 100, hozAlign: "right", formatter: "money" }, + { title: "발주품목수", field: "TOTAL_PO_PART_CNT", width: 100, hozAlign: "right", formatter: "money" }, + { title: "발주율(%)", field: "PO_RATE", width: 90, hozAlign: "right" }, + { title: "미발주품수", field: "NON_PO_PART_CNT", width: 90, hozAlign: "right", formatter: "money" }, + { title: "총수량", field: "TOTAL_BOM_PART_QTY_SUM", width: 90, hozAlign: "right", formatter: "money" }, + ], + }, + { + title: "입고현황", + headerHozAlign: "center", + columns: [ + { title: "발주수량(신)", field: "TOTAL_PO_NEW_QTY", width: 95, hozAlign: "right", formatter: "money" }, + { title: "발주수량(재)", field: "TOTAL_PO_RE_QTY", width: 95, hozAlign: "right", formatter: "money" }, + { title: "입고수량", field: "DELIVERY_QTY", width: 90, hozAlign: "right", formatter: "money" }, + { title: "미입고수량", field: "NON_DELIVERY_QTY", width: 90, hozAlign: "right", formatter: "money" }, + ], + }, + { + title: "수입검사결과(불량현황)", + headerHozAlign: "center", + columns: [ + { title: "부적합수량", field: "TOTAL_DEFECT_QTY", width: 90, hozAlign: "right", formatter: "money" }, + { title: "불량률(%)", field: "DELIVERY_RATE", width: 90, hozAlign: "right" }, + { title: "설계오류", field: "DEFECT_QTY_1", width: 80, hozAlign: "right", formatter: "money" }, + { title: "제작불량", field: "DEFECT_QTY_2", width: 80, hozAlign: "right", formatter: "money" }, + { title: "구매오류", field: "DEFECT_QTY_3", width: 80, hozAlign: "right", formatter: "money" }, + { title: "오품반입", field: "DEFECT_QTY_4", width: 80, hozAlign: "right", formatter: "money" }, + { title: "손실비용", field: "TOTAL_DEFECT_PRICE", width: 90, hozAlign: "right", formatter: "money" }, + ], + }, + ]; + + const fetchData = useCallback(async () => { + const res = await fetch("/api/delivery/status", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + Year: year, + customer_objid: customerObjid, + project_nos: projectNo ? [projectNo] : [], + }), + }); + if (res.ok) { + const json = await res.json(); + setData(json.RESULTLIST || []); + } else { + const j = await res.json().catch(() => ({})); + Swal.fire("오류", j.message || "조회 실패", "error"); + } + }, [year, customerObjid, projectNo]); + + useEffect(() => { + fetchData(); + // 최초 1회만 + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return ( +
+
+

입고관리_현황

+
+ +
+
+ + + + + + + + + + + + + + +
+ ); +} diff --git a/src/app/(main)/fund/expense-form/page.tsx b/src/app/(main)/fund/expense-form/page.tsx new file mode 100644 index 0000000..05c0f05 --- /dev/null +++ b/src/app/(main)/fund/expense-form/page.tsx @@ -0,0 +1,100 @@ +"use client"; + +import { useState, useCallback, useEffect } from "react"; +import { DataGrid, type GridColumn } from "@/components/grid/data-grid"; +import { SearchForm, SearchField } from "@/components/layout/search-form"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { CodeSelect } from "@/components/ui/code-select"; +import { ApprovalButton } from "@/components/approval/ApprovalButton"; + +// fundMgmt/fundExpenseFormList.jsp 대응 - 경비신청서관리 +export default function FundExpenseFormPage() { + const [year, setYear] = useState(new Date().getFullYear().toString()); + const [writerName, setWriterName] = useState(""); + const [statusCode, setStatusCode] = useState(""); + const [expenseDateFrom, setExpenseDateFrom] = useState(""); + const [expenseDateTo, setExpenseDateTo] = useState(""); + const [data, setData] = useState[]>([]); + const [selectedRows, setSelectedRows] = useState[]>([]); + + const columns: GridColumn[] = [ + { title: "신청번호", field: "EXPENSE_FORM_NO", width: 140, hozAlign: "left", + cellClick: (row) => window.open(`/fund/expense-form/form?objId=${row.OBJID}`, "expenseFormDetail", "width=1000,height=700") }, + { title: "프로젝트번호", field: "PROJECT_NO", width: 120, hozAlign: "left" }, + { title: "프로젝트명", field: "PROJECT_NAME", width: 200, hozAlign: "left" }, + { title: "신청자", field: "WRITER_NAME", width: 80, hozAlign: "center" }, + { title: "신청일", field: "EXPENSE_DATE", width: 100, hozAlign: "center" }, + { title: "경비구분", field: "EXPENSE_TYPE_NAME", width: 100, hozAlign: "center" }, + { title: "신청금액", field: "EXPENSE_AMOUNT", width: 120, hozAlign: "right", formatter: "money" }, + { title: "사용처", field: "EXPENSE_PLACE", width: 150, hozAlign: "left" }, + { title: "상태", field: "STATUS_NAME", width: 80, hozAlign: "center" }, + ]; + + const fetchData = useCallback(async () => { + const res = await fetch("/api/fund/expense-form", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + year, + writer_name: writerName, + status_code: statusCode, + expense_date_from: expenseDateFrom, + expense_date_to: expenseDateTo, + }), + }); + if (res.ok) { + const json = await res.json(); + setData(json.RESULTLIST || []); + } + }, [year, writerName, statusCode, expenseDateFrom, expenseDateTo]); + + // 페이지 진입 시 자동 로드 + useEffect(() => { fetchData(); }, [fetchData]); + + return ( +
+
+

경비신청서관리

+
+ + String(r.OBJID))} + targetType="EXPENSE_APPLY" + title={`경비 결재 요청 (${selectedRows.length}건)`} + description={selectedRows.map((r) => `${r.EXPENSE_ID} - ${r.BUS_TITLE}`).join("\n")} + onSuccess={fetchData} + disabled={selectedRows.length === 0} + /> + +
+
+ + + + + + + setWriterName(e.target.value)} className="w-[120px]" /> + + + + + + setExpenseDateFrom(e.target.value)} className="w-[140px]" /> + + + setExpenseDateTo(e.target.value)} className="w-[140px]" /> + + + + +
+ ); +} diff --git a/src/app/(main)/fund/invoice/page.tsx b/src/app/(main)/fund/invoice/page.tsx new file mode 100644 index 0000000..0bab97b --- /dev/null +++ b/src/app/(main)/fund/invoice/page.tsx @@ -0,0 +1,85 @@ +"use client"; + +import { useState, useCallback, useEffect } from "react"; +import { DataGrid, type GridColumn } from "@/components/grid/data-grid"; +import { SearchForm, SearchField } from "@/components/layout/search-form"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { CodeSelect } from "@/components/ui/code-select"; + +// fundMgmt/fundInvoiceList.jsp 대응 - 거래명세서관리 +export default function FundInvoicePage() { + const [year, setYear] = useState(new Date().getFullYear().toString()); + const [customerName, setCustomerName] = useState(""); + const [invoiceDateFrom, setInvoiceDateFrom] = useState(""); + const [invoiceDateTo, setInvoiceDateTo] = useState(""); + const [data, setData] = useState[]>([]); + + const columns: GridColumn[] = [ + { title: "거래명세서번호", field: "INVOICE_NO", width: 140, hozAlign: "left", + cellClick: (row) => window.open(`/fund/invoice/form?objId=${row.OBJID}`, "invoiceDetail", "width=1000,height=700") }, + { title: "고객사", field: "CUSTOMER_NAME", width: 150, hozAlign: "left" }, + { title: "프로젝트번호", field: "PROJECT_NO", width: 120, hozAlign: "left" }, + { title: "프로젝트명", field: "PROJECT_NAME", width: 200, hozAlign: "left" }, + { title: "발행일", field: "INVOICE_DATE", width: 100, hozAlign: "center" }, + { title: "공급가액", field: "SUPPLY_AMOUNT", width: 120, hozAlign: "right", formatter: "money" }, + { title: "세액", field: "TAX_AMOUNT", width: 100, hozAlign: "right", formatter: "money" }, + { title: "합계", field: "TOTAL_AMOUNT", width: 120, hozAlign: "right", formatter: "money" }, + { title: "상태", field: "STATUS_NAME", width: 80, hozAlign: "center" }, + { title: "등록자", field: "WRITER_NAME", width: 80, hozAlign: "center" }, + ]; + + const fetchData = useCallback(async () => { + const res = await fetch("/api/fund/invoice", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + year, + customer_name: customerName, + invoice_date_from: invoiceDateFrom, + invoice_date_to: invoiceDateTo, + }), + }); + if (res.ok) { + const json = await res.json(); + setData(json.RESULTLIST || []); + } + }, [year, customerName, invoiceDateFrom, invoiceDateTo]); + + // 페이지 진입 시 자동 로드 + useEffect(() => { fetchData(); }, [fetchData]); + + return ( +
+
+

거래명세서관리

+
+ + +
+
+ + + + + + + setCustomerName(e.target.value)} className="w-[150px]" /> + + + setInvoiceDateFrom(e.target.value)} className="w-[140px]" /> + + + setInvoiceDateTo(e.target.value)} className="w-[140px]" /> + + + + +
+ ); +} diff --git a/src/app/(main)/inventory/fund/page.tsx b/src/app/(main)/inventory/fund/page.tsx new file mode 100644 index 0000000..78c810b --- /dev/null +++ b/src/app/(main)/inventory/fund/page.tsx @@ -0,0 +1,101 @@ +"use client"; + +import { useState, useCallback, useEffect } from "react"; +import { DataGrid, type GridColumn } from "@/components/grid/data-grid"; +import { SearchForm, SearchField } from "@/components/layout/search-form"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { CodeSelect } from "@/components/ui/code-select"; +import Swal from "sweetalert2"; + +// 자재관리 > 자금관리 +export default function FundPage() { + const [year, setYear] = useState(new Date().getFullYear().toString()); + const [month, setMonth] = useState(""); + const [partnerObjid, setPartnerObjid] = useState(""); + const [paymentStatus, setPaymentStatus] = useState(""); + const [data, setData] = useState[]>([]); + const [selectedRows, setSelectedRows] = useState[]>([]); + + const columns: GridColumn[] = [ + { title: "공급업체", field: "PARTNER_NAME", width: 180, hozAlign: "left" }, + { title: "발주금액", field: "ORDER_AMOUNT", width: 120, hozAlign: "right", formatter: "money" }, + { title: "입고금액", field: "DELIVERY_AMOUNT", width: 120, hozAlign: "right", formatter: "money" }, + { title: "지급예정금액", field: "PAYMENT_PLAN_AMOUNT", width: 120, hozAlign: "right", formatter: "money" }, + { title: "지급완료금액", field: "PAYMENT_DONE_AMOUNT", width: 120, hozAlign: "right", formatter: "money" }, + { title: "미지급금액", field: "UNPAID_AMOUNT", width: 120, hozAlign: "right", formatter: "money" }, + { title: "지급예정일", field: "PAYMENT_PLAN_DATE", width: 100, hozAlign: "center" }, + { title: "지급상태", field: "PAYMENT_STATUS_NAME", width: 90, hozAlign: "center" }, + { title: "비고", field: "REMARK", width: 200, hozAlign: "left" }, + ]; + + const fetchData = useCallback(async () => { + const res = await fetch("/api/inventory/fund", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + year, + month, + partner_objid: partnerObjid, + payment_status: paymentStatus, + }), + }); + if (res.ok) { + const json = await res.json(); + setData(json.RESULTLIST || []); + } + }, [year, month, partnerObjid, paymentStatus]); + + const handleExcelDownload = () => { + Swal.fire("알림", "Excel 다운로드 기능은 준비 중입니다.", "info"); + }; + + // 페이지 진입 시 자동 로드 + useEffect(() => { fetchData(); }, [fetchData]); + + return ( +
+
+

자금관리

+
+ + +
+
+ + + + + + + + + + + + + + + + + +
+ ); +} diff --git a/src/app/(main)/inventory/history/page.tsx b/src/app/(main)/inventory/history/page.tsx new file mode 100644 index 0000000..3a2c0a2 --- /dev/null +++ b/src/app/(main)/inventory/history/page.tsx @@ -0,0 +1,43 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; +import { useSearchParams } from "next/navigation"; +import { DataGrid, type GridColumn } from "@/components/grid/data-grid"; +import { Button } from "@/components/ui/button"; + +// 재고 입출고 이력 팝업 +export default function InventoryHistoryPage() { + const searchParams = useSearchParams(); + const objId = searchParams.get("objId") || ""; + const [data, setData] = useState[]>([]); + + const fetchData = useCallback(async () => { + if (!objId) return; + const res = await fetch("/api/inventory/history", { + method: "POST", headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ parent_objid: objId }), + }); + if (res.ok) { const json = await res.json(); setData(json.RESULTLIST || []); } + }, [objId]); + + useEffect(() => { fetchData(); }, [fetchData]); + + const columns: GridColumn[] = [ + { title: "유형", field: "TYPE", width: 80, hozAlign: "center" }, + { title: "수량", field: "QTY", width: 100, hozAlign: "right", formatter: "money" }, + { title: "일자", field: "HIST_DATE", width: 110, hozAlign: "center" }, + { title: "위치", field: "LOCATION_NAME", width: 100, hozAlign: "center" }, + { title: "등록자", field: "WRITER_NAME", width: 90, hozAlign: "center" }, + { title: "비고", field: "REMARK", hozAlign: "left" }, + ]; + + return ( +
+

재고 입출고 이력

+ +
+ +
+
+ ); +} diff --git a/src/app/(main)/inventory/list/page.tsx b/src/app/(main)/inventory/list/page.tsx new file mode 100644 index 0000000..764662c --- /dev/null +++ b/src/app/(main)/inventory/list/page.tsx @@ -0,0 +1,407 @@ +"use client"; + +import { useState, useCallback, useEffect, useMemo } from "react"; +import { DataGrid, type GridColumn } from "@/components/grid/data-grid"; +import { SearchForm, SearchField } from "@/components/layout/search-form"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { SearchableCodeSelect } from "@/components/ui/searchable-code-select"; +import { SearchableSelect } from "@/components/ui/searchable-select"; +import Swal from "sweetalert2"; + +// 자재관리 > 자재리스트 (원본 /inventoryMng/inventoryMngNewList.do) +export default function InventoryListPage() { + const [projectNos, setProjectNos] = useState([]); + const [unitCode, setUnitCode] = useState(""); + const [partNo, setPartNo] = useState(""); + const [partName, setPartName] = useState(""); + const [partType, setPartType] = useState(""); + const [location, setLocation] = useState(""); + + const [projectOptions, setProjectOptions] = useState<{ value: string; label: string }[]>([]); + const [unitOptions, setUnitOptions] = useState<{ value: string; label: string }[]>([]); + + const [data, setData] = useState[]>([]); + const [selectedRows, setSelectedRows] = useState[]>([]); + const [loading, setLoading] = useState(false); + + // 프로젝트 옵션 로드 + useEffect(() => { + fetch("/api/common/project-list", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({}), + }) + .then((r) => r.json()) + .then((j) => { + const list = (j.RESULTLIST || []) as Array>; + setProjectOptions( + list.map((r) => ({ + value: String(r.OBJID || ""), + label: String(r.LABEL || r.PROJECT_NO || ""), + })), + ); + }) + .catch(() => setProjectOptions([])); + }, []); + + // 프로젝트 선택 변경 시 유닛 로드 (단일/다중 모두 대응 — 첫 번째 프로젝트 기준) + useEffect(() => { + setUnitCode(""); + if (projectNos.length === 0) { + setUnitOptions([]); + return; + } + const first = projectNos[0]; + fetch("/api/common/unit-list", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ contract_objid: first }), + }) + .then((r) => r.json()) + .then((j) => { + const list = (j.RESULTLIST || []) as Array>; + setUnitOptions( + list.map((r) => ({ + value: String(r.OBJID || ""), + label: String(r.UNIT_NAME || ""), + })), + ); + }) + .catch(() => setUnitOptions([])); + }, [projectNos]); + + const openHistoryPopup = useCallback((objId: string) => { + const w = 730; + const h = 400; + const left = (window.screen.width - w) / 2; + const top = (window.screen.height - h) / 2; + window.open( + `/inventory/request/history?objId=${encodeURIComponent(objId)}`, + "inventoryRequestHistoryPopUp", + `width=${w},height=${h},left=${left},top=${top}`, + ); + }, []); + + const columns: GridColumn[] = useMemo( + () => [ + { + title: "자재목록", + headerHozAlign: "center", + columns: [ + { title: "프로젝트번호", field: "PROJECT_NO", width: 100, hozAlign: "left", headerHozAlign: "center" }, + { title: "유닛명", field: "UNIT_NAME", width: 200, hozAlign: "left", headerHozAlign: "center" }, + { title: "품번", field: "PART_NO", width: 180, hozAlign: "left", headerHozAlign: "center" }, + { title: "품명", field: "PART_NAME", width: 180, hozAlign: "left", headerHozAlign: "center" }, + { title: "재질", field: "MATERIAL", width: 180, hozAlign: "left", headerHozAlign: "center" }, + { title: "사양(규격)", field: "SPEC", width: 200, hozAlign: "left", headerHozAlign: "center" }, + { title: "PART구분", field: "PART_TYPE_NAME", width: 120, hozAlign: "center", headerHozAlign: "center" }, + { title: "보유수량", field: "USE_CNT", width: 100, hozAlign: "center", headerHozAlign: "center", formatter: "money" }, + { title: "보유수량(전체)", field: "USE_CNT_ALL", width: 120, hozAlign: "center", headerHozAlign: "center", formatter: "money" }, + { title: "Location", field: "LOCATION_NAME", width: 120, hozAlign: "left", headerHozAlign: "center" }, + ], + }, + { + title: "불출이력", + headerHozAlign: "center", + columns: [ + { + title: "불출이력", + field: "REQUEST_QTY", + width: 80, + hozAlign: "center", + headerHozAlign: "center", + formatter: (cell, row) => { + const v = Number(cell || 0); + if (v === 0) return "0"; + return ( + { + e.preventDefault(); + openHistoryPopup(String(row.OBJID || "")); + }} + className="text-blue-600 underline" + > + {v.toLocaleString()} + + ); + }, + }, + { title: "비고", field: "REMARK", width: 200, hozAlign: "left", headerHozAlign: "center" }, + ], + }, + ], + [openHistoryPopup], + ); + + const fetchData = useCallback(async () => { + setLoading(true); + try { + const res = await fetch("/api/inventory/list", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + project_nos: projectNos.join(","), + unit_code: unitCode, + part_no: partNo, + part_name: partName, + part_type: partType, + location, + }), + }); + if (res.ok) { + const json = await res.json(); + setData(json.RESULTLIST || []); + } + } finally { + setLoading(false); + } + }, [projectNos, unitCode, partNo, partName, partType, location]); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + // 등록 팝업 (inventoryFormPopUp) + const handleRegister = () => { + const w = 850; + const h = 500; + const left = (window.screen.width - w) / 2; + const top = (window.screen.height - h) / 2; + window.open( + "/inventory/list/form", + "inventoryFormPopUp", + `width=${w},height=${h},left=${left},top=${top}`, + ); + }; + + // 자재이동 (materialMoveFormPopUp) + const handleMove = () => { + if (selectedRows.length === 0) { + Swal.fire("알림", "선택된 데이터가 없습니다.", "warning"); + return; + } + for (const r of selectedRows) { + if (Number(r.USE_CNT || 0) === 0) { + alert("보유수량이 0일 경우 이동이 불가능합니다."); + return; + } + } + const checkArr = selectedRows.map((r) => String(r.OBJID)).join(","); + const w = 1600; + const h = 700; + const left = (window.screen.width - w) / 2; + const top = (window.screen.height - h) / 2; + window.open( + `/inventory/move/form?checkArr=${encodeURIComponent(checkArr)}`, + "materialMoveFormPopUp", + `width=${w},height=${h},left=${left},top=${top}`, + ); + }; + + // 불출의뢰 (materialRequestFormPopUp) + const handleRequest = () => { + if (selectedRows.length === 0) { + Swal.fire("알림", "선택된 데이터가 없습니다.", "warning"); + return; + } + for (const r of selectedRows) { + if (Number(r.USE_CNT || 0) === 0) { + alert("보유수량이 0일 경우 불출의뢰가 불가능합니다."); + return; + } + } + const checkArr = selectedRows.map((r) => String(r.OBJID)).join(","); + const w = 1800; + const h = 700; + const left = (window.screen.width - w) / 2; + const top = (window.screen.height - h) / 2; + window.open( + `/inventory/request/form?checkArr=${encodeURIComponent(checkArr)}`, + "inventoryRequestPopUp", + `width=${w},height=${h},left=${left},top=${top}`, + ); + }; + + return ( +
+
+

자재관리_자재리스트

+
+ + + + +
+
+ + + + + + + + + + setPartNo(e.target.value)} + placeholder="품번" + className="w-[150px]" + /> + + + setPartName(e.target.value)} + placeholder="품명" + className="w-[170px]" + /> + + + + + + + + + + +
+ ); +} + +// 간단한 다중 선택 — 선택된 라벨을 태그로 표시 + 드롭다운 +function MultiSelect({ + options, + value, + onChange, + placeholder, + className, +}: { + options: { value: string; label: string }[]; + value: string[]; + onChange: (v: string[]) => void; + placeholder?: string; + className?: string; +}) { + const [open, setOpen] = useState(false); + const [search, setSearch] = useState(""); + + const filtered = options.filter((o) => { + if (!search) return true; + const s = search.toLowerCase(); + return o.label.toLowerCase().includes(s) || o.value.toLowerCase().includes(s); + }); + + const toggle = (v: string) => { + if (value.includes(v)) onChange(value.filter((x) => x !== v)); + else onChange([...value, v]); + }; + + const selectedLabels = value + .map((v) => options.find((o) => o.value === v)?.label || v) + .join(", "); + + return ( +
+ + {value.length > 0 && ( + + )} + {open && ( + <> +
setOpen(false)} /> +
+
+ setSearch(e.target.value)} + placeholder="검색" + className="w-full px-2 py-1 text-sm border rounded" + /> +
+ {filtered.length === 0 ? ( +
결과 없음
+ ) : ( + filtered.map((o) => { + const selected = value.includes(o.value); + return ( + + ); + }) + )} +
+ + )} +
+ ); +} diff --git a/src/app/(main)/inventory/page.tsx b/src/app/(main)/inventory/page.tsx new file mode 100644 index 0000000..a7f401f --- /dev/null +++ b/src/app/(main)/inventory/page.tsx @@ -0,0 +1,125 @@ +"use client"; + +import { useState, useCallback, useEffect } from "react"; +import { DataGrid, type GridColumn } from "@/components/grid/data-grid"; +import { SearchForm, SearchField } from "@/components/layout/search-form"; +import { CodeSelect } from "@/components/ui/code-select"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; + +// inventoryMngInputList.jsp 대응 - 재고관리(입고) +export default function InventoryPage() { + const [year, setYear] = useState(new Date().getFullYear().toString()); + const [projectNo, setProjectNo] = useState(""); + const [partNo, setPartNo] = useState(""); + const [partName, setPartName] = useState(""); + const [spec, setSpec] = useState(""); + const [clsCd, setClsCd] = useState(""); + const [cauCd, setCauCd] = useState(""); + const [location, setLocation] = useState(""); + const [data, setData] = useState[]>([]); + const [totalCount, setTotalCount] = useState(0); + + // inventoryMngInputList.jsp columns 대응 + const columns: GridColumn[] = [ + { title: "프로젝트번호", field: "PROJECT_NO", width: 100, hozAlign: "left" }, + { title: "유닛명", field: "UNIT_NAME", hozAlign: "left" }, + { title: "파트번호", field: "PART_NO", width: 130, hozAlign: "left" }, + { title: "파트명", field: "PART_NAME", width: 150, hozAlign: "left" }, + { title: "규격", field: "SPEC", width: 110, hozAlign: "left" }, + { title: "업체", field: "MAKER", width: 100, hozAlign: "left" }, + { title: "재고구분", field: "CLS_CD_NAME", width: 90, hozAlign: "center" }, + { title: "발생사유", field: "CAU_CD_NAME", width: 100, hozAlign: "center" }, + { title: "발생수량", field: "QTY", width: 90, hozAlign: "right", formatter: "money" }, + { title: "위치", field: "LOCATION_NAME", width: 100, hozAlign: "left" }, + { title: "금액(원)", field: "PRICE", width: 100, hozAlign: "right", formatter: "money" }, + { title: "등록일", field: "REG_DATE", width: 90, hozAlign: "center" }, + { title: "등록자", field: "WRITER_NAME", width: 90, hozAlign: "center" }, + { title: "총입고수량", field: "INPUT_QTY", width: 90, hozAlign: "center", + cellClick: (row) => openHistoryPopup(String(row.OBJID)) }, + { title: "최종입고일", field: "INPUT_DATE", width: 90, hozAlign: "center" }, + { title: "잔여수량", field: "REMAIN_QTY", width: 90, hozAlign: "center", + cellClick: (row) => openHistoryPopup(String(row.OBJID)) }, + ]; + + const fetchData = useCallback(async () => { + const res = await fetch("/api/inventory", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + year, project_no: projectNo, part_no: partNo, part_name: partName, + spec, cls_cd: clsCd, cau_cd: cauCd, location, + }), + }); + if (res.ok) { + const json = await res.json(); + setData(json.RESULTLIST || []); + setTotalCount(json.TOTAL_CNT || 0); + } + }, [year, projectNo, partNo, partName, spec, clsCd, cauCd, location]); + + const openHistoryPopup = (objId: string) => { + window.open(`/inventory/history?objId=${objId}`, "inventoryHistory", "width=600,height=500"); + }; + + const openInputPopup = () => { + window.open("/inventory/input-form", "inventoryInput", "width=850,height=330"); + }; + + // 페이지 진입 시 자동 로드 + useEffect(() => { fetchData(); }, [fetchData]); + + return ( +
+
+

재고관리 (입고)

+
+ + +
+
+ + + + + + + + setPartNo(e.target.value)} className="w-[120px]" /> + + + + setPartName(e.target.value)} className="w-[120px]" /> + + + + setSpec(e.target.value)} className="w-[100px]" /> + + + + + + + + + + + + +
+ ); +} diff --git a/src/app/(main)/inventory/request/history/page.tsx b/src/app/(main)/inventory/request/history/page.tsx new file mode 100644 index 0000000..3055012 --- /dev/null +++ b/src/app/(main)/inventory/request/history/page.tsx @@ -0,0 +1,138 @@ +"use client"; + +import { useState, useEffect, useCallback, Suspense } from "react"; +import { useSearchParams } from "next/navigation"; +import { Button } from "@/components/ui/button"; +import { numberWithCommas } from "@/lib/utils"; + +// 입출고 History 팝업 (원본 /inventoryMng/inventoryRequestHistoryPopUp.do) +function HistoryPage() { + const searchParams = useSearchParams(); + const objId = searchParams.get("objId") || searchParams.get("partId") || ""; + const [rows, setRows] = useState[]>([]); + + const fetchData = useCallback(async () => { + if (!objId) return; + const res = await fetch("/api/inventory/request/history", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ objId }), + }); + if (res.ok) { + const json = await res.json(); + setRows(json.RESULTLIST || []); + } + }, [objId]); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + const openTarget = (id: string, gubun: string) => { + if (gubun === "출고") { + const w = 1500; + const h = 700; + const left = (window.screen.width - w) / 2; + const top = (window.screen.height - h) / 2; + const params = new URLSearchParams({ + INVENTORY_REQUEST_MASTER_OBJID: id, + action: "view", + }); + window.open( + `/inventory/request/detail?${params.toString()}`, + "inventoryRequestPopUp", + `width=${w},height=${h},left=${left},top=${top}`, + ); + } else if (gubun === "입고") { + const w = 1260; + const h = 1050; + const left = (window.screen.width - w) / 2; + const top = (window.screen.height - h) / 2; + window.open( + `/delivery/acceptance/form?objId=${encodeURIComponent(id)}&actionType=view`, + "deliveryAcceptancePopUp", + `width=${w},height=${h},left=${left},top=${top}`, + ); + } + }; + + return ( +
+
+

입출고 이력

+
+ +
+ + + + + + + + + + + + + + {rows.length === 0 ? ( + + + + ) : ( + rows.map((r, i) => { + const gubun = String(r.GUBUN || ""); + const rowObjId = String(r.OBJID || ""); + return ( + + + + + + + + + + ); + }) + )} + +
프로젝트번호품번품명구분입출고수량LocationSub_Location
+ 조회된 데이터가 없습니다. +
{String(r.PROJECT_NO || "")}{String(r.PART_NO || "")}{String(r.PART_NAME || "")} + {gubun === "입고" || gubun === "출고" ? ( + { + e.preventDefault(); + openTarget(rowObjId, gubun); + }} + className="text-blue-600 underline" + > + {gubun} + + ) : ( + {gubun} + )} + + {numberWithCommas(String(r.RECEIPT_QTY || ""))} + {String(r.LOCATION_NAME || "")}{String(r.SUB_LOCATION_NAME || "")}
+
+ +
+ +
+
+ ); +} + +export default function Page() { + return ( + 로딩 중...
}> + + + ); +} diff --git a/src/app/(main)/inventory/request/page.tsx b/src/app/(main)/inventory/request/page.tsx new file mode 100644 index 0000000..237fa6b --- /dev/null +++ b/src/app/(main)/inventory/request/page.tsx @@ -0,0 +1,341 @@ +"use client"; + +import { useState, useCallback, useEffect, useMemo } from "react"; +import { DataGrid, type GridColumn } from "@/components/grid/data-grid"; +import { SearchForm, SearchField } from "@/components/layout/search-form"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { SearchableSelect } from "@/components/ui/searchable-select"; +import { ExcelDownloadButton } from "@/components/ui/excel-download-button"; +import Swal from "sweetalert2"; + +// 자재관리 > 불출의뢰서 (원본 /inventoryMng/materialRequestList.do) +export default function InventoryRequestPage() { + const [partNo, setPartNo] = useState(""); + const [partName, setPartName] = useState(""); + const [requestStartDate, setRequestStartDate] = useState(""); + const [requestEndDate, setRequestEndDate] = useState(""); + const [requestUser, setRequestUser] = useState(""); + const [receptionStatus, setReceptionStatus] = useState(""); + const [receptionUser, setReceptionUser] = useState(""); + const [receptionStartDate, setReceptionStartDate] = useState(""); + const [receptionEndDate, setReceptionEndDate] = useState(""); + const [outStatus, setOutStatus] = useState(""); + + const [userOptions, setUserOptions] = useState<{ value: string; label: string }[]>([]); + const [data, setData] = useState[]>([]); + const [selectedRows, setSelectedRows] = useState[]>([]); + const [loading, setLoading] = useState(false); + + useEffect(() => { + fetch("/api/admin/users", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({}), + }) + .then((r) => r.json()) + .then((j) => { + const list = (j.RESULTLIST || []) as Array>; + setUserOptions( + list.map((r) => ({ + value: String(r.USER_ID || ""), + label: String(r.USER_NAME || r.USER_ID || ""), + })), + ); + }) + .catch(() => setUserOptions([])); + }, []); + + const openDetailPopup = useCallback( + (objId: string, outStatusTitle: string, receptionStatusTitle: string) => { + const w = 1800; + const h = 700; + const left = (window.screen.width - w) / 2; + const top = (window.screen.height - h) / 2; + const params = new URLSearchParams({ + INVENTORY_REQUEST_MASTER_OBJID: objId, + action: "view", + OUTSTATUS: outStatusTitle, + RECEPTION_STATUS: receptionStatusTitle, + }); + window.open( + `/inventory/request/detail?${params.toString()}`, + "inventoryRequestPopUp", + `width=${w},height=${h},left=${left},top=${top}`, + ); + }, + [], + ); + + const columns: GridColumn[] = useMemo( + () => [ + { + title: "자재불출번호", + field: "INVENTORY_OUT_NO", + width: 140, + hozAlign: "left", + headerHozAlign: "center", + formatter: (cell, row) => ( + { + e.preventDefault(); + openDetailPopup( + String(row.OBJID || ""), + String(row.OUTSTATUS_TITLE || ""), + String(row.RECEPTION_STATUS_TITLE || ""), + ); + }} + className="text-blue-600 underline" + > + {String(cell || "")} + + ), + }, + { title: "품번", field: "PART_NO_ARR", width: 480, hozAlign: "left", headerHozAlign: "center" }, + { title: "품명", field: "PART_NAME_ARR", width: 480, hozAlign: "left", headerHozAlign: "center" }, + { title: "불출의뢰일", field: "REQUEST_DATE", width: 120, hozAlign: "left", headerHozAlign: "center" }, + { title: "의뢰자", field: "REQUEST_USER_NAME", width: 100, hozAlign: "left", headerHozAlign: "center" }, + { title: "상태", field: "RECEPTION_STATUS_TITLE", width: 100, hozAlign: "left", headerHozAlign: "center" }, + { title: "접수자", field: "RECEPTION_USER_NAME", width: 100, hozAlign: "center", headerHozAlign: "center" }, + { title: "접수일", field: "RECEPTION_DATE", width: 100, hozAlign: "center", headerHozAlign: "center" }, + { title: "불출상태", field: "OUTSTATUS_TITLE", width: 100, hozAlign: "center", headerHozAlign: "center" }, + ], + [openDetailPopup], + ); + + const fetchData = useCallback(async () => { + setLoading(true); + try { + const res = await fetch("/api/inventory/request", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + part_no: partNo, + part_name: partName, + request_start_date: requestStartDate, + request_end_date: requestEndDate, + request_user: requestUser, + reception_status: receptionStatus, + reception_user: receptionUser, + reception_start_date: receptionStartDate, + reception_end_date: receptionEndDate, + out_status: outStatus, + }), + }); + if (res.ok) { + const json = await res.json(); + setData(json.RESULTLIST || []); + } + } finally { + setLoading(false); + } + }, [ + partNo, + partName, + requestStartDate, + requestEndDate, + requestUser, + receptionStatus, + receptionUser, + receptionStartDate, + receptionEndDate, + outStatus, + ]); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + // 접수 (미접수만 가능) + const handleReceipt = async () => { + if (selectedRows.length === 0) { + Swal.fire("선택된 데이터가 없습니다."); + return; + } + const checkedArr = selectedRows + .filter((r) => String(r.RECEPTION_STATUS_TITLE) === "미접수") + .map((r) => String(r.OBJID)); + if (checkedArr.length === 0) { + Swal.fire("선택된 데이터가 없습니다."); + return; + } + const confirmed = await Swal.fire({ + title: "선택된 데이터를 접수하시겠습니까?", + icon: "warning", + showCancelButton: true, + confirmButtonText: "확인", + cancelButtonText: "취소", + confirmButtonColor: "#3085d6", + cancelButtonColor: "#d33", + }); + if (!confirmed.isConfirmed) return; + + const res = await fetch("/api/inventory/request/receipt", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ checkArr: checkedArr.join(",") }), + }); + const data = await res.json(); + Swal.fire(data.message || "접수되었습니다."); + if (data.success) fetchData(); + }; + + // 자재불출 (단일 선택, 접수 상태만) + const handleAccept = () => { + if (selectedRows.length === 0) { + Swal.fire("선택된 데이터가 없습니다."); + return; + } + if (selectedRows.length > 1) { + Swal.fire("한번에 1개의 내용만 불출가능합니다."); + return; + } + const row = selectedRows[0]; + const receptionStatusTitle = String(row.RECEPTION_STATUS_TITLE || ""); + const outStatusTitle = String(row.OUTSTATUS_TITLE || ""); + + if (receptionStatusTitle !== "접수") { + Swal.fire("접수한 데이터만 불출가능합니다."); + return; + } + if (outStatusTitle === "완료") { + Swal.fire("불출완료된 데이터는 불출가능합니다."); + return; + } + + const w = 1800; + const h = 700; + const left = (window.screen.width - w) / 2; + const top = (window.screen.height - h) / 2; + const params = new URLSearchParams({ + INVENTORY_REQUEST_MASTER_OBJID: String(row.OBJID || ""), + OUTSTATUS: outStatusTitle, + RECEPTION_STATUS: receptionStatusTitle, + }); + window.open( + `/inventory/request/detail?${params.toString()}`, + "inventoryRequestPopUp", + `width=${w},height=${h},left=${left},top=${top}`, + ); + }; + + return ( +
+
+

자재관리_불출의뢰서

+
+ + + + +
+
+ + + + setPartNo(e.target.value)} + placeholder="품번" + className="w-[150px]" + /> + + + setPartName(e.target.value)} + placeholder="품명" + className="w-[150px]" + /> + + +
+ setRequestStartDate(e.target.value)} + className="w-[130px]" + /> + ~ + setRequestEndDate(e.target.value)} + className="w-[130px]" + /> +
+
+ + + + + + + + + + +
+ setReceptionStartDate(e.target.value)} + className="w-[130px]" + /> + ~ + setReceptionEndDate(e.target.value)} + className="w-[130px]" + /> +
+
+ + + +
+ + +
+ ); +} diff --git a/src/app/(main)/inventory/status/page.tsx b/src/app/(main)/inventory/status/page.tsx new file mode 100644 index 0000000..4e3fc68 --- /dev/null +++ b/src/app/(main)/inventory/status/page.tsx @@ -0,0 +1,92 @@ +"use client"; + +import { useState, useCallback, useEffect } from "react"; +import { DataGrid, type GridColumn } from "@/components/grid/data-grid"; +import { SearchForm, SearchField } from "@/components/layout/search-form"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { CodeSelect } from "@/components/ui/code-select"; +import Swal from "sweetalert2"; + +// 자재관리 > 현황 +export default function InventoryStatusPage() { + const [year, setYear] = useState(new Date().getFullYear().toString()); + const [projectNo, setProjectNo] = useState(""); + const [locationCd, setLocationCd] = useState(""); + const [partTypeCd, setPartTypeCd] = useState(""); + const [data, setData] = useState[]>([]); + + const columns: GridColumn[] = [ + { title: "프로젝트번호", field: "PROJECT_NO", width: 130, hozAlign: "left" }, + { title: "유닛명", field: "UNIT_NAME", width: 120, hozAlign: "center" }, + { title: "부품구분", field: "PART_TYPE_NAME", width: 90, hozAlign: "center" }, + { title: "위치", field: "LOCATION_NAME", width: 100, hozAlign: "center" }, + { title: "총보유수량", field: "TOTAL_QTY", width: 100, hozAlign: "right", formatter: "money" }, + { title: "불출수량", field: "REQUEST_QTY", width: 90, hozAlign: "right", formatter: "money" }, + { title: "잔여수량", field: "REMAIN_QTY", width: 90, hozAlign: "right", formatter: "money" }, + { title: "입고수량", field: "DELIVERY_QTY", width: 90, hozAlign: "right", formatter: "money" }, + { title: "이동수량", field: "MOVE_QTY", width: 90, hozAlign: "right", formatter: "money" }, + ]; + + const fetchData = useCallback(async () => { + const res = await fetch("/api/inventory/status", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + year, + project_no: projectNo, + location_cd: locationCd, + part_type_cd: partTypeCd, + }), + }); + if (res.ok) { + const json = await res.json(); + setData(json.RESULTLIST || []); + } + }, [year, projectNo, locationCd, partTypeCd]); + + const handleExcelDownload = () => { + Swal.fire("알림", "Excel 다운로드 기능은 준비 중입니다.", "info"); + }; + + // 페이지 진입 시 자동 로드 + useEffect(() => { fetchData(); }, [fetchData]); + + return ( +
+
+

자재현황

+
+ + +
+
+ + + + + + + setProjectNo(e.target.value)} placeholder="프로젝트번호" className="w-[150px]" /> + + + + + + + + + + +
+ ); +} diff --git a/src/app/(main)/layout.tsx b/src/app/(main)/layout.tsx new file mode 100644 index 0000000..04c5474 --- /dev/null +++ b/src/app/(main)/layout.tsx @@ -0,0 +1,48 @@ +"use client"; + +import { useEffect } from "react"; +import { useRouter } from "next/navigation"; +import { useAuthStore } from "@/store/auth-store"; +import { Sidebar } from "@/components/layout/sidebar"; +import { Header } from "@/components/layout/header"; +import { Loading } from "@/components/ui/loading"; + +// mainFS.jsp 대응 - 프레임셋 → Sidebar + Header + Content 레이아웃 +export default function MainLayout({ children }: { children: React.ReactNode }) { + const router = useRouter(); + const { user, isLoading, fetchUser } = useAuthStore(); + + useEffect(() => { + fetchUser(); + }, [fetchUser]); + + useEffect(() => { + if (!isLoading && !user) { + router.push("/login"); + } + }, [isLoading, user, router]); + + if (isLoading) { + return ; + } + + if (!user) return null; + + return ( +
+ {/* 사이드바 (menu.jsp 대응) */} + + + {/* 메인 영역 */} +
+ {/* 헤더 (header.jsp 대응) */} +
+ + {/* 콘텐츠 (contents_page iframe 대응) */} +
+ {children} +
+
+
+ ); +} diff --git a/src/app/(main)/order/amount-status/page.tsx b/src/app/(main)/order/amount-status/page.tsx new file mode 100644 index 0000000..cf1ce71 --- /dev/null +++ b/src/app/(main)/order/amount-status/page.tsx @@ -0,0 +1,170 @@ +"use client"; + +import { useState, useCallback, useEffect } from "react"; +import { DataGrid, type GridColumn } from "@/components/grid/data-grid"; +import { SearchForm, SearchField } from "@/components/layout/search-form"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { SearchableSelect } from "@/components/ui/searchable-select"; +import { numberWithCommas } from "@/lib/utils"; +import Swal from "sweetalert2"; + +type Option = { value: string; label: string }; + +// 원본: /purchaseOrder/purchaseOrderStatusAmountBySupply.do +// 발주관리 > 업체별_입고요청월 금액현황 +export default function AmountStatusPage() { + const [year, setYear] = useState(new Date().getFullYear().toString()); + const [projectNo, setProjectNo] = useState(""); + const [partnerObjid, setPartnerObjid] = useState(""); + const [salesMngUserId, setSalesMngUserId] = useState(""); + const [data, setData] = useState[]>([]); + const [sums, setSums] = useState>({}); + const [loading, setLoading] = useState(false); + + const [supplyOptions, setSupplyOptions] = useState([]); + const [userOptions, setUserOptions] = useState([]); + + useEffect(() => { + fetch("/api/admin/supply", { method: "POST", headers: { "Content-Type": "application/json" }, body: "{}" }) + .then((r) => r.json()) + .then((d) => + setSupplyOptions( + (d.RESULTLIST || []).map((r: Record) => ({ + value: String(r.OBJID), + label: String(r.SUPPLY_NAME), + })), + ), + ) + .catch(() => {}); + + fetch("/api/admin/users", { method: "POST", headers: { "Content-Type": "application/json" }, body: "{}" }) + .then((r) => r.json()) + .then((d) => + setUserOptions( + (d.RESULTLIST || []).map((r: Record) => ({ + value: String(r.USER_ID), + label: String(r.USER_NAME), + })), + ), + ) + .catch(() => {}); + }, []); + + const columns: GridColumn[] = [ + { title: "공급업체", field: "SUPPLY_NAME", width: 180, hozAlign: "left", frozen: true }, + { title: "1월", field: "M01", width: 110, hozAlign: "right", formatter: "money" }, + { title: "2월", field: "M02", width: 110, hozAlign: "right", formatter: "money" }, + { title: "3월", field: "M03", width: 110, hozAlign: "right", formatter: "money" }, + { title: "4월", field: "M04", width: 110, hozAlign: "right", formatter: "money" }, + { title: "5월", field: "M05", width: 110, hozAlign: "right", formatter: "money" }, + { title: "6월", field: "M06", width: 110, hozAlign: "right", formatter: "money" }, + { title: "7월", field: "M07", width: 110, hozAlign: "right", formatter: "money" }, + { title: "8월", field: "M08", width: 110, hozAlign: "right", formatter: "money" }, + { title: "9월", field: "M09", width: 110, hozAlign: "right", formatter: "money" }, + { title: "10월", field: "M10", width: 110, hozAlign: "right", formatter: "money" }, + { title: "11월", field: "M11", width: 110, hozAlign: "right", formatter: "money" }, + { title: "12월", field: "M12", width: 110, hozAlign: "right", formatter: "money" }, + { title: "합계", field: "TOTAL_SUPPLY_UNIT_PRICE", width: 140, hozAlign: "right", formatter: "money" }, + ]; + + const fetchData = useCallback(async () => { + setLoading(true); + try { + const res = await fetch("/api/order/amount-status", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + year, + project_no: projectNo, + partner_objid: partnerObjid, + sales_mng_user_id: salesMngUserId, + }), + }); + if (res.ok) { + const json = await res.json(); + setData(json.RESULTLIST || []); + setSums(json.SUMS || {}); + } else { + const j = await res.json().catch(() => ({})); + Swal.fire("오류", j.message || "조회 실패", "error"); + } + } finally { + setLoading(false); + } + }, [year, projectNo, partnerObjid, salesMngUserId]); + + const handleReset = () => { + setYear(new Date().getFullYear().toString()); + setProjectNo(""); + setPartnerObjid(""); + setSalesMngUserId(""); + }; + + const handleExcelDownload = () => { + Swal.fire("알림", "Excel 다운로드 기능은 준비 중입니다.", "info"); + }; + + useEffect(() => { + fetchData(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return ( +
+
+

업체별 입고요청월 금액현황

+
+ + + +
+
+ + + + + + + setProjectNo(e.target.value)} + placeholder="프로젝트번호 부분 일치" + className="w-[180px]" + /> + + + + + + + + + +
+ 총 합계(원) : {numberWithCommas(Number(sums.TOTAL_SUPPLY_UNIT_PRICE ?? 0))} +
+ + +
+ ); +} diff --git a/src/app/(main)/order/list/page.tsx b/src/app/(main)/order/list/page.tsx new file mode 100644 index 0000000..e30b5d5 --- /dev/null +++ b/src/app/(main)/order/list/page.tsx @@ -0,0 +1,292 @@ +"use client"; + +import { useState, useCallback, useEffect } from "react"; +import { DataGrid, type GridColumn } from "@/components/grid/data-grid"; +import { SearchForm, SearchField } from "@/components/layout/search-form"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { SearchableCodeSelect } from "@/components/ui/searchable-code-select"; +import { SearchableSelect } from "@/components/ui/searchable-select"; +import Swal from "sweetalert2"; +import { ApprovalButton } from "@/components/approval/ApprovalButton"; + +type Option = { value: string; label: string }; + +// 원본: /purchaseOrder/purchaseOrderList_new.do +export default function OrderListPage() { + const [year, setYear] = useState(""); + const [customerCd, setCustomerCd] = useState(""); + const [projectNo, setProjectNo] = useState(""); + const [unitCode, setUnitCode] = useState(""); + const [purchaseOrderNo, setPurchaseOrderNo] = useState(""); + const [type, setType] = useState(""); + const [orderTypeCd, setOrderTypeCd] = useState(""); + const [deliveryStartDate, setDeliveryStartDate] = useState(""); + const [deliveryEndDate, setDeliveryEndDate] = useState(""); + const [partnerObjid, setPartnerObjid] = useState(""); + const [salesMngUserId, setSalesMngUserId] = useState(""); + const [regStartDate, setRegStartDate] = useState(""); + const [regEndDate, setRegEndDate] = useState(""); + const [partNo, setPartNo] = useState(""); + const [partName, setPartName] = useState(""); + const [poClientId, setPoClientId] = useState(""); + const [apprStatus, setApprStatus] = useState(""); + const [partSpec, setPartSpec] = useState(""); + + const [customerOptions, setCustomerOptions] = useState([]); + const [supplyOptions, setSupplyOptions] = useState([]); + const [userOptions, setUserOptions] = useState([]); + + const [data, setData] = useState[]>([]); + const [selectedRows, setSelectedRows] = useState[]>([]); + + useEffect(() => { + fetch("/api/common/supply-list", { method: "POST", headers: { "Content-Type": "application/json" }, body: "{}" }) + .then((r) => r.json()) + .then((d) => + setCustomerOptions( + (d.RESULTLIST || []).map((r: Record) => ({ + value: String(r.OBJID), + label: String(r.SUPPLY_NAME), + })), + ), + ) + .catch(() => {}); + + fetch("/api/admin/supply", { method: "POST", headers: { "Content-Type": "application/json" }, body: "{}" }) + .then((r) => r.json()) + .then((d) => + setSupplyOptions( + (d.RESULTLIST || []).map((r: Record) => ({ + value: String(r.OBJID), + label: String(r.SUPPLY_NAME), + })), + ), + ) + .catch(() => {}); + + fetch("/api/admin/users", { method: "POST", headers: { "Content-Type": "application/json" }, body: "{}" }) + .then((r) => r.json()) + .then((d) => + setUserOptions( + (d.RESULTLIST || []).map((r: Record) => ({ + value: String(r.USER_ID), + label: String(r.USER_NAME), + })), + ), + ) + .catch(() => {}); + }, []); + + const openOrderForm = (objId?: string) => { + const url = objId ? `/order/list/form?objId=${objId}&action=view` : "/order/list/form"; + const w = 1460; + const h = 1050; + const left = (window.screen.width - w) / 2; + const top = (window.screen.height - h) / 2; + window.open(url, `orderForm_${objId || "new"}`, `width=${w},height=${h},left=${left},top=${top}`); + }; + + const columns: GridColumn[] = [ + { + title: "발주No", + field: "PURCHASE_ORDER_NO", + width: 130, + hozAlign: "left", + frozen: true, + cellClick: (row) => openOrderForm(String(row.OBJID || "")), + }, + { title: "년도", field: "CM_YEAR", width: 60, hozAlign: "center" }, + { title: "고객사", field: "CUSTOMER_NAME", width: 130, hozAlign: "left" }, + { title: "고객사프로젝트명", field: "CUSTOMER_PROJECT_NAME", width: 180, hozAlign: "left" }, + { title: "당사프로젝트번호", field: "PROJECT_NO", width: 130, hozAlign: "left" }, + { title: "부품", field: "TYPE_NAME", width: 70, hozAlign: "center" }, + { title: "구분", field: "ORDER_TYPE_CD_NAME", width: 70, hozAlign: "center" }, + { title: "동시", field: "MULTI_YN_MAKED", width: 50, hozAlign: "center" }, + { title: "발주서제목", field: "TITLE", width: 220, hozAlign: "left" }, + { title: "납품장소", field: "DELIVERY_PLACE_NAME", width: 100, hozAlign: "center" }, + { title: "검수방법", field: "INSPECT_METHOD_NAME", width: 90, hozAlign: "center" }, + { title: "결재조건", field: "PAYMENT_TERMS_NAME", width: 110, hozAlign: "center" }, + { title: "입고요청일", field: "DELIVERY_DATE", width: 100, hozAlign: "center" }, + { title: "공급업체", field: "PARTNER_NAME", width: 140, hozAlign: "left" }, + { title: "구매담당", field: "SALES_MNG_USER_NAME", width: 80, hozAlign: "center" }, + { title: "발주일", field: "REGDATE", width: 100, hozAlign: "center" }, + { title: "발주금액", field: "TOTAL_PRICE_ALL", width: 110, hozAlign: "right", formatter: "money" }, + { title: "상태", field: "APPR_STATUS_NAME", width: 80, hozAlign: "center" }, + ]; + + const fetchData = useCallback(async () => { + const res = await fetch("/api/order/list", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + year, + customer_cd: customerCd, + project_no: projectNo, + unit_code: unitCode, + purchase_order_no: purchaseOrderNo, + type, + order_type_cd: orderTypeCd, + delivery_start_date: deliveryStartDate, + delivery_end_date: deliveryEndDate, + partner_objid: partnerObjid, + sales_mng_user_id: salesMngUserId, + reg_start_date: regStartDate, + reg_end_date: regEndDate, + part_no: partNo, + part_name: partName, + po_client_id: poClientId, + appr_status: apprStatus, + part_spec: partSpec, + }), + }); + if (res.ok) { + const json = await res.json(); + setData(json.RESULTLIST || []); + } else { + const j = await res.json().catch(() => ({})); + Swal.fire("오류", j.message || "조회 실패", "error"); + } + }, [ + year, customerCd, projectNo, unitCode, purchaseOrderNo, type, orderTypeCd, + deliveryStartDate, deliveryEndDate, partnerObjid, salesMngUserId, + regStartDate, regEndDate, partNo, partName, poClientId, apprStatus, partSpec, + ]); + + const masterSelectedIds = selectedRows + .filter((r) => r.MULTI_YN !== "Y" || r.MULTI_MASTER_YN === "Y") + .map((r) => String(r.OBJID)); + + useEffect(() => { + fetchData(); + // 최초 1회만 + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return ( +
+
+

발주서관리

+
+ + + `${r.PURCHASE_ORDER_NO} - ${r.TITLE}`).join("\n")} + onSuccess={fetchData} + disabled={masterSelectedIds.length === 0} + /> +
+
+ + + + + + + + + + setProjectNo(e.target.value)} className="w-[150px]" /> + + + setUnitCode(e.target.value)} className="w-[130px]" placeholder="유닛코드" /> + + + setPurchaseOrderNo(e.target.value)} className="w-[140px]" /> + + + + + + + + +
+ setDeliveryStartDate(e.target.value)} + className="h-9 rounded border border-gray-300 bg-white px-2 text-sm w-[130px]" + /> + ~ + setDeliveryEndDate(e.target.value)} + className="h-9 rounded border border-gray-300 bg-white px-2 text-sm w-[130px]" + /> +
+
+ + + + + + + +
+ setRegStartDate(e.target.value)} + className="h-9 rounded border border-gray-300 bg-white px-2 text-sm w-[130px]" + /> + ~ + setRegEndDate(e.target.value)} + className="h-9 rounded border border-gray-300 bg-white px-2 text-sm w-[130px]" + /> +
+
+ + setPartNo(e.target.value)} className="w-[130px]" /> + + + setPartName(e.target.value)} className="w-[130px]" /> + + + + + + + + + setPartSpec(e.target.value)} className="w-[130px]" /> + +
+ + +
+ ); +} diff --git a/src/app/(main)/order/status/page.tsx b/src/app/(main)/order/status/page.tsx new file mode 100644 index 0000000..ac0b01a --- /dev/null +++ b/src/app/(main)/order/status/page.tsx @@ -0,0 +1,167 @@ +"use client"; + +import { useState, useCallback, useEffect } from "react"; +import { DataGrid, type GridColumn } from "@/components/grid/data-grid"; +import { SearchForm, SearchField } from "@/components/layout/search-form"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { FolderCell } from "@/components/ui/folder-cell"; +import { numberWithCommas } from "@/lib/utils"; +import Swal from "sweetalert2"; + +const openBomPopup = (objId: string) => { + if (!objId) return; + const w = 1800, h = 800; + const left = (window.screen.width - w) / 2; + const top = (window.screen.height - h) / 2; + window.open( + `/purchase/bom/structure?objId=${objId}&readonly=1`, + `bomStructure_${objId}`, + `width=${w},height=${h},left=${left},top=${top}`, + ); +}; + +// 원본: /purchaseOrder/purchaseOrderStatusByProject.do (발주관리_현황) +export default function OrderStatusPage() { + const [year, setYear] = useState(""); + const [projectNo, setProjectNo] = useState(""); + const [data, setData] = useState[]>([]); + const [sums, setSums] = useState>({}); + const [loading, setLoading] = useState(false); + + const columns: GridColumn[] = [ + { + title: "프로젝트정보", + columns: [ + { title: "고객사", field: "CUSTOMER_NAME", width: 110, hozAlign: "left", frozen: true }, + { title: "프로젝트번호", field: "PROJECT_NO", width: 110, hozAlign: "left", frozen: true }, + { title: "유닛명", field: "UNIT_PART_NAME", width: 220, hozAlign: "left" }, + { title: "프로젝트명", field: "CUSTOMER_PROJECT_NAME", width: 130, hozAlign: "left" }, + ], + }, + { + title: "발주현황", + columns: [ + { + title: "구매BOM", + field: "BOM_CNT", + width: 80, + hozAlign: "center", + formatter: (cell, row) => ( + openBomPopup(String(row.BOM_REPORT_OBJID || ""))} + /> + ), + }, + { title: "전체수량", field: "TOTAL_BOM_PART_CNT", width: 80, hozAlign: "right", formatter: "money" }, + { title: "발주품수", field: "TOTAL_PO_PART_CNT", width: 80, hozAlign: "right", formatter: "money" }, + { title: "미발주품수", field: "NON_PO_PART_CNT", width: 80, hozAlign: "right", formatter: "money" }, + { + title: "발주율", + field: "RATE_PO", + width: 80, + hozAlign: "right", + formatter: (cell) => `${Number(cell) || 0}%`, + }, + { title: "구매품", field: "PRICE_PT_1", width: 100, hozAlign: "right", formatter: "money" }, + { title: "제작품", field: "PRICE_PT_2", width: 100, hozAlign: "right", formatter: "money" }, + { title: "사급품", field: "PRICE_PT_3", width: 100, hozAlign: "right", formatter: "money" }, + { title: "기타", field: "PRICE_PT_ETC", width: 100, hozAlign: "right", formatter: "money" }, + ], + }, + { + title: "재발주현황", + columns: [ + { title: "건수", field: "RE_COUNT", width: 70, hozAlign: "right", formatter: "money" }, + { title: "금액(원)", field: "RE_PRICE", width: 120, hozAlign: "right", formatter: "money" }, + ], + }, + { + title: "턴키현황", + columns: [ + { title: "건수", field: "TURNKEY_COUNT", width: 70, hozAlign: "right", formatter: "money" }, + { title: "금액(원)", field: "TURNKEY_PRICE", width: 120, hozAlign: "right", formatter: "money" }, + ], + }, + { title: "총발주금액(원)", field: "TOTAL_PRICE_ALL", width: 140, hozAlign: "right", formatter: "money" }, + ]; + + const fetchData = useCallback(async () => { + setLoading(true); + try { + const res = await fetch("/api/order/status", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ year, project_no: projectNo }), + }); + if (res.ok) { + const json = await res.json(); + setData(json.RESULTLIST || []); + setSums(json.SUMS || {}); + } else { + const j = await res.json().catch(() => ({})); + Swal.fire("오류", j.message || "조회 실패", "error"); + } + } finally { + setLoading(false); + } + }, [year, projectNo]); + + const handleReset = () => { + setYear(""); + setProjectNo(""); + }; + + const handleExcelDownload = () => { + Swal.fire("알림", "Excel 다운로드 기능은 준비 중입니다.", "info"); + }; + + useEffect(() => { + fetchData(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return ( +
+
+

발주관리_현황

+
+ + + +
+
+ + + + + + + setProjectNo(e.target.value)} + placeholder="프로젝트번호 부분 일치" + className="w-[200px]" + /> + + + +
+ 총발주금액(원) : {numberWithCommas(Number(sums.TOTAL_PRICE_ALL_SUM ?? 0))} + 단일발주금액(원) : {numberWithCommas(Number(sums.SINGLE_PRICE_SUM ?? 0))} +
+ + +
+ ); +} diff --git a/src/app/(main)/part-mgmt/page.tsx b/src/app/(main)/part-mgmt/page.tsx new file mode 100644 index 0000000..5349e1c --- /dev/null +++ b/src/app/(main)/part-mgmt/page.tsx @@ -0,0 +1,77 @@ +"use client"; + +import { useState, useCallback, useEffect } from "react"; +import { DataGrid, type GridColumn } from "@/components/grid/data-grid"; +import { SearchForm, SearchField } from "@/components/layout/search-form"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { CodeSelect } from "@/components/ui/code-select"; + +// partmgmt/partmgmtList.jsp 대응 - 부품관리(PART) +export default function PartMgmtPage() { + const [partNo, setPartNo] = useState(""); + const [partName, setPartName] = useState(""); + const [partType, setPartType] = useState(""); + const [data, setData] = useState[]>([]); + + const columns: GridColumn[] = [ + { title: "파트번호", field: "PART_NO", width: 150, hozAlign: "left", + cellClick: (row) => openPartDetail(String(row.OBJID)) }, + { title: "파트명", field: "PART_NAME", width: 200, hozAlign: "left" }, + { title: "파트유형", field: "PART_TYPE_NAME", width: 100, hozAlign: "center" }, + { title: "규격", field: "SPEC", width: 130, hozAlign: "left" }, + { title: "재질", field: "MATERIAL_NAME", width: 100, hozAlign: "left" }, + { title: "UNIT", field: "UNIT_NAME", width: 70, hozAlign: "center" }, + { title: "중량", field: "WEIGHT", width: 80, hozAlign: "right" }, + { title: "업체", field: "MAKER", width: 120, hozAlign: "left" }, + { title: "등록자", field: "WRITER_NAME", width: 90, hozAlign: "center" }, + { title: "등록일", field: "REGDATE", width: 100, hozAlign: "center" }, + { title: "2D", field: "DRAWING_2D_CNT", width: 40, hozAlign: "center" }, + { title: "3D", field: "DRAWING_3D_CNT", width: 40, hozAlign: "center" }, + ]; + + const fetchData = useCallback(async () => { + const res = await fetch("/api/part-mgmt", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ part_no: partNo, part_name: partName, part_type: partType }), + }); + if (res.ok) { + const json = await res.json(); + setData(json.RESULTLIST || []); + } + }, [partNo, partName, partType]); + + const openPartDetail = (objId: string) => { + window.open(`/product-mgmt/form?objId=${objId}`, "partDetail", "width=1100,height=800"); + }; + + // 페이지 진입 시 자동 로드 + useEffect(() => { fetchData(); }, [fetchData]); + + return ( +
+
+

부품관리 (PART)

+
+ + +
+
+ + + + setPartNo(e.target.value)} className="w-[150px]" /> + + + setPartName(e.target.value)} className="w-[150px]" /> + + + + + + + +
+ ); +} diff --git a/src/app/(main)/part/register/page.tsx b/src/app/(main)/part/register/page.tsx new file mode 100644 index 0000000..4bb34c3 --- /dev/null +++ b/src/app/(main)/part/register/page.tsx @@ -0,0 +1,84 @@ +"use client"; + +import { useState, useCallback, useEffect } from "react"; +import { DataGrid, type GridColumn } from "@/components/grid/data-grid"; +import { SearchForm, SearchField } from "@/components/layout/search-form"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { CodeSelect } from "@/components/ui/code-select"; + +// partMgmt/partRegisterList.jsp 대응 - Part 등록 +export default function PartRegisterPage() { + const [partNo, setPartNo] = useState(""); + const [partName, setPartName] = useState(""); + const [spec, setSpec] = useState(""); + const [maker, setMaker] = useState(""); + const [categoryCode, setCategoryCode] = useState(""); + const [data, setData] = useState[]>([]); + + const columns: GridColumn[] = [ + { title: "파트번호", field: "PART_NO", width: 140, hozAlign: "left", + cellClick: (row) => window.open(`/product/part-register/form?objId=${row.OBJID}`, "partDetail", "width=900,height=600") }, + { title: "파트명", field: "PART_NAME", width: 180, hozAlign: "left" }, + { title: "규격", field: "SPEC", width: 150, hozAlign: "left" }, + { title: "단위", field: "UNIT_NAME", width: 60, hozAlign: "center" }, + { title: "카테고리", field: "CATEGORY_NAME", width: 100, hozAlign: "center" }, + { title: "제조사", field: "MAKER", width: 120, hozAlign: "left" }, + { title: "단가", field: "UNIT_PRICE", width: 100, hozAlign: "right", formatter: "money" }, + { title: "등록일", field: "REG_DATE", width: 100, hozAlign: "center" }, + { title: "등록자", field: "WRITER_NAME", width: 80, hozAlign: "center" }, + ]; + + const fetchData = useCallback(async () => { + const res = await fetch("/api/part/register", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + part_no: partNo, + part_name: partName, + spec, + maker, + category_code: categoryCode, + }), + }); + if (res.ok) { + const json = await res.json(); + setData(json.RESULTLIST || []); + } + }, [partNo, partName, spec, maker, categoryCode]); + + // 페이지 진입 시 자동 로드 + useEffect(() => { fetchData(); }, [fetchData]); + + return ( +
+
+

Part 등록

+
+ + +
+
+ + + + setPartNo(e.target.value)} className="w-[140px]" /> + + + setPartName(e.target.value)} className="w-[150px]" /> + + + setSpec(e.target.value)} className="w-[120px]" /> + + + setMaker(e.target.value)} className="w-[120px]" /> + + + + + + + +
+ ); +} diff --git a/src/app/(main)/procurement-std/code1/page.tsx b/src/app/(main)/procurement-std/code1/page.tsx new file mode 100644 index 0000000..bc8672b --- /dev/null +++ b/src/app/(main)/procurement-std/code1/page.tsx @@ -0,0 +1,13 @@ +"use client"; + +import StandardCodePage from "@/components/procurement-std/StandardCodePage"; + +// 구매품표준관리 - 코드1 +export default function Code1Page() { + return ( + + ); +} diff --git a/src/app/(main)/procurement-std/code2/page.tsx b/src/app/(main)/procurement-std/code2/page.tsx new file mode 100644 index 0000000..2156308 --- /dev/null +++ b/src/app/(main)/procurement-std/code2/page.tsx @@ -0,0 +1,13 @@ +"use client"; + +import StandardCodePage from "@/components/procurement-std/StandardCodePage"; + +// 구매품표준관리 - 코드2 +export default function Code2Page() { + return ( + + ); +} diff --git a/src/app/(main)/procurement-std/code3/page.tsx b/src/app/(main)/procurement-std/code3/page.tsx new file mode 100644 index 0000000..4181a4e --- /dev/null +++ b/src/app/(main)/procurement-std/code3/page.tsx @@ -0,0 +1,13 @@ +"use client"; + +import StandardCodePage from "@/components/procurement-std/StandardCodePage"; + +// 구매품표준관리 - 코드3 +export default function Code3Page() { + return ( + + ); +} diff --git a/src/app/(main)/procurement-std/code4/page.tsx b/src/app/(main)/procurement-std/code4/page.tsx new file mode 100644 index 0000000..ae866d7 --- /dev/null +++ b/src/app/(main)/procurement-std/code4/page.tsx @@ -0,0 +1,13 @@ +"use client"; + +import StandardCodePage from "@/components/procurement-std/StandardCodePage"; + +// 구매품표준관리 - 코드4 +export default function Code4Page() { + return ( + + ); +} diff --git a/src/app/(main)/procurement-std/code5/page.tsx b/src/app/(main)/procurement-std/code5/page.tsx new file mode 100644 index 0000000..cf6160b --- /dev/null +++ b/src/app/(main)/procurement-std/code5/page.tsx @@ -0,0 +1,13 @@ +"use client"; + +import StandardCodePage from "@/components/procurement-std/StandardCodePage"; + +// 구매품표준관리 - 코드5 +export default function Code5Page() { + return ( + + ); +} diff --git a/src/app/(main)/procurement-std/material-code/page.tsx b/src/app/(main)/procurement-std/material-code/page.tsx new file mode 100644 index 0000000..94b2508 --- /dev/null +++ b/src/app/(main)/procurement-std/material-code/page.tsx @@ -0,0 +1,13 @@ +"use client"; + +import StandardCodePage from "@/components/procurement-std/StandardCodePage"; + +// 구매품표준관리 - 자재코드 +export default function MaterialCodePage() { + return ( + + ); +} diff --git a/src/app/(main)/product-mgmt/page.tsx b/src/app/(main)/product-mgmt/page.tsx new file mode 100644 index 0000000..931a7ab --- /dev/null +++ b/src/app/(main)/product-mgmt/page.tsx @@ -0,0 +1,149 @@ +"use client"; + +import { useState, useCallback, useEffect } from "react"; +import { DataGrid, type GridColumn } from "@/components/grid/data-grid"; +import { SearchForm, SearchField } from "@/components/layout/search-form"; +import { CodeSelect } from "@/components/ui/code-select"; +import { Button } from "@/components/ui/button"; +import { Pagination } from "@/components/ui/pagination"; +import Swal from "sweetalert2"; + +// productmgmtList.jsp 대응 - 제품마스터 +export default function ProductMgmtPage() { + const [year, setYear] = useState(new Date().getFullYear().toString()); + const [productCategory, setProductCategory] = useState(""); + const [data, setData] = useState[]>([]); + const [totalCount, setTotalCount] = useState(0); + const [currentPage, setCurrentPage] = useState(1); + const [selectedRows, setSelectedRows] = useState[]>([]); + const countPerPage = 20; + + const columns: GridColumn[] = [ + { title: "사업부", field: "PRODUCT_CATEGORY_NAME", width: 120, + cellClick: (row) => openProductFormPopup(String(row.OBJID)) }, + { title: "제품군", field: "PRODUCT_TYPE_NAME", width: 100, hozAlign: "left" }, + { title: "제품명", field: "PRODUCT_NAME", width: 180, hozAlign: "left" }, + { title: "제품코드", field: "PRODUCT_CODE", width: 130, hozAlign: "left" }, + { title: "등록자", field: "WRITER_NAME", width: 100, hozAlign: "center" }, + { title: "등록일", field: "REGDATE", width: 110, hozAlign: "center" }, + { title: "생산여부", field: "PRODUCTION_FLAG_NAME", width: 100, hozAlign: "center" }, + { title: "첨부", field: "FILE_CNT", width: 60, hozAlign: "center", + formatter: (_cell, row) => Number(row.FILE_CNT || 0) > 0 ? "📎" : "" }, + ]; + + const fetchData = useCallback(async (page = 1) => { + const res = await fetch("/api/product-mgmt", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ year, product_category: productCategory, page, countPerPage }), + }); + if (res.ok) { + const json = await res.json(); + setData(json.RESULTLIST || []); + setTotalCount(json.TOTAL_CNT || 0); + setCurrentPage(page); + } + }, [year, productCategory]); + + const handleSearch = () => { + fetchData(1); + }; + + const handleDelete = async () => { + if (selectedRows.length === 0) { + Swal.fire("선택한 항목이 없습니다."); + return; + } + + const result = await Swal.fire({ + title: "삭제 확인", + text: `${selectedRows.length}건을 삭제하시겠습니까?`, + icon: "warning", + showCancelButton: true, + confirmButtonText: "삭제", + cancelButtonText: "취소", + }); + + if (result.isConfirmed) { + const res = await fetch("/api/product-mgmt", { + method: "DELETE", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ objIds: selectedRows.map((r) => r.OBJID) }), + }); + const json = await res.json(); + if (json.success) { + Swal.fire({ icon: "success", title: "삭제되었습니다.", timer: 1500, showConfirmButton: false }); + fetchData(currentPage); + } else { + Swal.fire({ icon: "error", title: json.message || "삭제 실패" }); + } + } + }; + + const openProductFormPopup = (objId?: string) => { + // TODO: 모달 다이얼로그로 대체 (기존 window.open popup 대응) + const url = objId + ? `/product-mgmt/form?objId=${objId}` + : `/product-mgmt/form?actionType=regist`; + window.open(url, "productForm", "width=850,height=480"); + }; + + // 페이지 진입 시 자동 로드 + useEffect(() => { fetchData(); }, [fetchData]); + + return ( +
+
+

제품마스터

+
+ + + +
+
+ + {/* 검색 영역 (productmgmtList.jsp #plmSearchZon 대응) */} + + + + + + + + + + + {/* 그리드 (Tabulator/plm_table 대응) */} + + + {/* 페이징 */} + fetchData(page)} + /> +
+ ); +} diff --git a/src/app/(main)/product/bom-list/page.tsx b/src/app/(main)/product/bom-list/page.tsx new file mode 100644 index 0000000..42dc440 --- /dev/null +++ b/src/app/(main)/product/bom-list/page.tsx @@ -0,0 +1,318 @@ +"use client"; + +import { useState, useCallback, useEffect, useMemo } from "react"; +import { DataGrid, type GridColumn } from "@/components/grid/data-grid"; +import { SearchForm, SearchField } from "@/components/layout/search-form"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { SearchableSelect } from "@/components/ui/searchable-select"; +import { FolderCell } from "@/components/ui/folder-cell"; +import * as XLSX from "xlsx"; +import Swal from "sweetalert2"; + +type Row = Record; + +// 제품관리_BOM 조회 (원본: partMng/structureAscendingList.jsp) +// 고객사 → 프로젝트번호 → 유닛명 캐스케이드 + 품번/LEVEL 필터로 BOM 정전개 트리 조회 +const LEVEL_COLORS = [ + "", "#fde9d9", "#daeef3", "#e4dfec", "#ebf1de", "#f2f2f2", + "#f2dcdb", "#eeece1", "#dce6f1", "#FFFFEB", "#ffffff", +]; + +export default function BomListPage() { + const [customerCd, setCustomerCd] = useState(""); + const [projectName, setProjectName] = useState(""); + const [unitCode, setUnitCode] = useState(""); + const [searchPartNo, setSearchPartNo] = useState(""); + const [searchLevel, setSearchLevel] = useState(""); + + const [data, setData] = useState([]); + const [maxLevel, setMaxLevel] = useState(0); + const [loading, setLoading] = useState(false); + // 접힌 노드: CHILD_OBJID 를 키로 사용 (해당 노드의 모든 하위 행을 숨김) + const [collapsed, setCollapsed] = useState>(new Set()); + + const [customers, setCustomers] = useState<{ value: string; label: string }[]>([]); + const [projects, setProjects] = useState<{ value: string; label: string }[]>([]); + const [units, setUnits] = useState<{ value: string; label: string }[]>([]); + + // 고객사 로드 + useEffect(() => { + fetch("/api/common/supply-list", { + method: "POST", headers: { "Content-Type": "application/json" }, body: "{}", + }).then((r) => r.json()) + .then((j) => setCustomers((j.RESULTLIST || []).map((r: Row) => ({ + value: String(r.OBJID), label: String(r.SUPPLY_NAME || r.OBJID), + })))) + .catch(() => {}); + }, []); + + // 고객사 변경 → 프로젝트 + useEffect(() => { + setProjectName(""); setUnitCode(""); + fetch("/api/common/project-list", { + method: "POST", headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ customer_cd: customerCd }), + }).then((r) => r.json()) + .then((j) => setProjects((j.RESULTLIST || []).map((r: Row) => ({ + value: String(r.OBJID), label: String(r.LABEL || r.PROJECT_NO || r.OBJID), + })))) + .catch(() => {}); + }, [customerCd]); + + // 프로젝트 변경 → 유닛 + useEffect(() => { + setUnitCode(""); + if (!projectName) { setUnits([]); return; } + fetch("/api/common/unit-list", { + method: "POST", headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ contract_objid: projectName }), + }).then((r) => r.json()) + .then((j) => setUnits((j.RESULTLIST || []).map((r: Row) => ({ + value: String(r.OBJID), label: String(r.UNIT_NAME || r.TASK_NAME), + })))) + .catch(() => {}); + }, [projectName]); + + const openPartDetail = (partObjId: string) => { + const w = 600, h = 700; + const left = (window.screen.width - w) / 2; + const top = (window.screen.height - h) / 2; + window.open(`/product/popup/part-form?objId=${encodeURIComponent(partObjId)}&readOnly=true`, + "partMngDetail", `width=${w},height=${h},left=${left},top=${top},scrollbars=yes,resizable=yes`); + }; + + const openFilePopup = (objId: string, docType: string, docTypeName: string) => { + const w = 800, h = 335; + const left = (window.screen.width - w) / 2; + const top = (window.screen.height - h) / 2; + window.open( + `/common/files?targetObjId=${encodeURIComponent(objId)}&docType=${encodeURIComponent(docType)}&docTypeName=${encodeURIComponent(docTypeName)}`, + "fileRegistPopUp", `width=${w},height=${h},left=${left},top=${top}`, + ); + }; + + // 조상 체인 계산용 인덱스 (CHILD_OBJID → data index) + const indexByChildObjid = useMemo(() => { + const m = new Map(); + data.forEach((r, i) => m.set(String(r.CHILD_OBJID), i)); + return m; + }, [data]); + + // 각 row 의 조상 CHILD_OBJID 리스트 (루트→자신 바로 위) + const ancestorsOf = useCallback((row: Row): string[] => { + const chain: string[] = []; + let cur: Row | undefined = row; + while (cur && cur.PARENT_OBJID) { + const p = String(cur.PARENT_OBJID); + chain.push(p); + const idx = indexByChildObjid.get(p); + cur = idx !== undefined ? data[idx] : undefined; + } + return chain; + }, [data, indexByChildObjid]); + + // 접힌 조상이 하나라도 있으면 숨김 + const visibleData = useMemo( + () => (collapsed.size === 0 + ? data + : data.filter((r) => !ancestorsOf(r).some((a) => collapsed.has(a)))), + [data, collapsed, ancestorsOf], + ); + + const toggleNode = useCallback((childObjid: string) => { + setCollapsed((prev) => { + const next = new Set(prev); + if (next.has(childObjid)) next.delete(childObjid); + else next.add(childObjid); + return next; + }); + }, []); + + const expandAll = () => setCollapsed(new Set()); + const collapseAllLevel1 = () => { + // LEV 1 중 자식이 있는 노드만 접기 (원본 JSP 기본 동작 상응) + const ids = new Set(); + data.forEach((r) => { + if (Number(r.LEV ?? 0) === 1 && Number(r.LEAF ?? 1) === 0) { + ids.add(String(r.CHILD_OBJID)); + } + }); + setCollapsed(ids); + }; + + // 레벨 컬럼 동적 생성 (1, 2, 3 ... MAX_LEVEL) + const levelColumns: GridColumn[] = Array.from({ length: Math.max(maxLevel, 1) }, (_, i) => ({ + title: String(i + 1), field: `_LEV_${i + 1}`, width: 22, hozAlign: "center", + formatter: (_cell, row) => Number(row.LEV ?? 0) === i + 1 ? "*" : "", + })); + + const columns: GridColumn[] = [ + { + title: "", field: "_TREE", width: 28, hozAlign: "center", + formatter: (_cell, row) => { + if (Number(row.LEAF ?? 1) !== 0) return ""; + const id = String(row.CHILD_OBJID); + const isCollapsed = collapsed.has(id); + return ( + + ); + }, + }, + ...levelColumns, + { + title: "품번", field: "PART_NO", width: 140, hozAlign: "left", + cellClick: (row) => openPartDetail(String(row.PART_OBJID)), + }, + { title: "품명", field: "PART_NAME", width: 180, hozAlign: "left" }, + { title: "수량", field: "QTY", width: 55, hozAlign: "center" }, + { + title: "3D", field: "FILE_3D_CNT", width: 40, hozAlign: "center", + formatter: (cell) => , + cellClick: (row) => openFilePopup(String(row.PART_OBJID), "3D_CAD", "3D CAD 첨부파일"), + }, + { + title: "2D", field: "FILE_2D_CNT", width: 40, hozAlign: "center", + formatter: (cell) => , + cellClick: (row) => openFilePopup(String(row.PART_OBJID), "2D_DRAWING_CAD", "2D(Drawing) CAD 첨부파일"), + }, + { + title: "PDF", field: "FILE_PDF_CNT", width: 40, hozAlign: "center", + formatter: (cell) => , + cellClick: (row) => openFilePopup(String(row.PART_OBJID), "2D_PDF_CAD", "2D(PDF) CAD 첨부파일"), + }, + { title: "재질", field: "MATERIAL", width: 100, hozAlign: "left" }, + { title: "사양(규격)", field: "SPEC", width: 130, hozAlign: "left" }, + { title: "후처리", field: "POST_PROCESSING", width: 120, hozAlign: "left" }, + { title: "MAKER", field: "MAKER", width: 90, hozAlign: "left" }, + { title: "Revision", field: "REVISION", width: 70, hozAlign: "center" }, + { title: "EO No", field: "EO_NO", width: 85, hozAlign: "center" }, + { title: "EO Date", field: "EO_DATE", width: 90, hozAlign: "center" }, + { title: "PART구분", field: "PART_TYPE_TITLE", width: 90, hozAlign: "center" }, + { title: "비고", field: "REMARK", width: 200, hozAlign: "left" }, + ]; + + const validate = () => { + // 품번만 입력해도 검색 허용 — 품번이 있으면 고객사/프로젝트/유닛 미선택 가능 + if (searchPartNo) return true; + if (!customerCd) { Swal.fire("알림", "고객사를 선택하거나 품번을 입력해 주세요.", "warning"); return false; } + if (!projectName) { Swal.fire("알림", "프로젝트번호를 선택해 주세요.", "warning"); return false; } + if (!unitCode) { Swal.fire("알림", "유닛명을 선택해 주세요.", "warning"); return false; } + return true; + }; + + const fetchData = useCallback(async (skipValidate = false) => { + if (!skipValidate && !validate()) return; + if (!searchPartNo && (!customerCd || !projectName || !unitCode)) return; + setLoading(true); + try { + const res = await fetch("/api/product/bom/ascending", { + method: "POST", headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + customer_cd: customerCd, + project_name: projectName, + unit_code: unitCode, + search_partNo: searchPartNo, + search_level: searchLevel, + }), + }); + if (res.ok) { + const j = await res.json(); + setData(j.RESULTLIST || []); + setMaxLevel(Number(j.MAX_LEVEL || 0)); + setCollapsed(new Set()); + } + } finally { + setLoading(false); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [customerCd, projectName, unitCode, searchPartNo, searchLevel]); + + const handleReset = () => { + setCustomerCd(""); setProjectName(""); setUnitCode(""); + setSearchPartNo(""); setSearchLevel(""); + setData([]); setMaxLevel(0); + setCollapsed(new Set()); + }; + + const handleExcel = () => { + if (data.length === 0) { Swal.fire("알림", "다운로드할 데이터가 없습니다.", "warning"); return; } + const levelHeaders = Array.from({ length: Math.max(maxLevel, 1) }, (_, i) => String(i + 1)); + const header = [ + ...levelHeaders, "품번", "품명", "수량", "3D", "2D", "PDF", + "재질", "사양(규격)", "후처리", "MAKER", "Revision", + "EO No", "EO Date", "PART구분", "비고", + ]; + const body = data.map((r) => { + const lev = Number(r.LEV ?? 0); + const markers = Array.from({ length: Math.max(maxLevel, 1) }, (_, i) => (lev === i + 1 ? "*" : "")); + return [ + ...markers, r.PART_NO ?? "", r.PART_NAME ?? "", r.QTY ?? "", + Number(r.FILE_3D_CNT ?? 0), Number(r.FILE_2D_CNT ?? 0), Number(r.FILE_PDF_CNT ?? 0), + r.MATERIAL ?? "", r.SPEC ?? "", r.POST_PROCESSING ?? "", + r.MAKER ?? "", r.REVISION ?? "", r.EO_NO ?? "", + r.EO_DATE ?? "", r.PART_TYPE_TITLE ?? "", r.REMARK ?? "", + ]; + }); + const ws = XLSX.utils.aoa_to_sheet([header, ...body]); + const wb = XLSX.utils.book_new(); + XLSX.utils.book_append_sheet(wb, ws, "BOM_REPORT_정전개"); + const today = new Date().toISOString().slice(0, 10); + XLSX.writeFile(wb, `BOM_REPORT_정전개_${today}.xlsx`); + }; + + // 행별 배경색(레벨별) + const rowClass = (row: Row): string => { + const lev = Number(row.LEV ?? 0); + const bg = LEVEL_COLORS[Math.min(lev, LEVEL_COLORS.length - 1)]; + return bg ? "" : ""; + void bg; // 배경색은 인라인 스타일 쓰지 않고 기본 유지 (선택적) + }; + void rowClass; + + return ( +
+
+

제품관리_BOM 조회

+
+ + + + + +
+
+ + fetchData()}> + + + + + + + + + + + setSearchPartNo(e.target.value)} className="w-[200px]" /> + + + + + + + +
+ ); +} diff --git a/src/app/(main)/product/bom-register/page.tsx b/src/app/(main)/product/bom-register/page.tsx new file mode 100644 index 0000000..f8f4c9f --- /dev/null +++ b/src/app/(main)/product/bom-register/page.tsx @@ -0,0 +1,305 @@ +"use client"; + +import { useState, useCallback, useEffect } from "react"; +import { DataGrid, type GridColumn } from "@/components/grid/data-grid"; +import { SearchForm, SearchField } from "@/components/layout/search-form"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { SearchableSelect } from "@/components/ui/searchable-select"; +import { FolderCell } from "@/components/ui/folder-cell"; +import * as XLSX from "xlsx"; +import Swal from "sweetalert2"; + +// 제품관리_PART 및 구조등록 (원본: partMng/structureList.jsp) +export default function BomRegisterPage() { + const [customerCd, setCustomerCd] = useState(""); + const [projectName, setProjectName] = useState(""); + const [unitCode, setUnitCode] = useState(""); + const [searchUnitName, setSearchUnitName] = useState(""); + const [searchWriter, setSearchWriter] = useState(""); + const [deployFrom, setDeployFrom] = useState(""); + const [deployTo, setDeployTo] = useState(""); + const [status, setStatus] = useState(""); + + const [data, setData] = useState[]>([]); + const [loading, setLoading] = useState(false); + const [selectedRows, setSelectedRows] = useState[]>([]); + const [customers, setCustomers] = useState<{ value: string; label: string }[]>([]); + const [projects, setProjects] = useState<{ value: string; label: string }[]>([]); + const [units, setUnits] = useState<{ value: string; label: string }[]>([]); + const [users, setUsers] = useState<{ value: string; label: string }[]>([]); + + // 고객사 목록 (supply_mng) + useEffect(() => { + fetch("/api/common/supply-list", { + method: "POST", headers: { "Content-Type": "application/json" }, body: "{}", + }).then((r) => r.json()) + .then((j) => setCustomers((j.RESULTLIST || []).map((r: Record) => ({ + value: String(r.OBJID), label: String(r.SUPPLY_NAME || r.OBJID), + })))) + .catch(() => {}); + }, []); + + useEffect(() => { + fetch("/api/admin/users", { method: "POST", headers: { "Content-Type": "application/json" }, body: "{}" }) + .then((r) => r.json()) + .then((j) => setUsers((j.RESULTLIST || []).map((r: Record) => ({ + value: String(r.USER_ID), label: String(r.USER_NAME || ""), + })))) + .catch(() => {}); + }, []); + + // 고객사 변경 시 프로젝트 로드 + useEffect(() => { + setProjectName(""); setUnitCode(""); + fetch("/api/common/project-list", { + method: "POST", headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ customer_cd: customerCd }), + }).then((r) => r.json()) + .then((j) => setProjects((j.RESULTLIST || []).map((r: Record) => ({ + value: String(r.OBJID), label: String(r.LABEL || r.PROJECT_NO || r.OBJID), + })))) + .catch(() => {}); + }, [customerCd]); + + // 프로젝트 변경 시 유닛 로드 + useEffect(() => { + setUnitCode(""); + if (!projectName) { setUnits([]); return; } + fetch("/api/common/unit-list", { + method: "POST", headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ contract_objid: projectName }), + }).then((r) => r.json()) + .then((j) => setUnits((j.RESULTLIST || []).map((r: Record) => ({ + value: String(r.OBJID), label: String(r.UNIT_NAME || r.TASK_NAME), + })))) + .catch(() => {}); + }, [projectName]); + + const openChangeDesignNote = (objIds: string) => { + const w = 1000, h = 200; + const left = (window.screen.width - w) / 2; + const top = (window.screen.height - h) / 2; + window.open( + `/product/popup/change-design-note?objId=${encodeURIComponent(objIds)}`, + "changeDesignNotePopUp", + `width=${w},height=${h},left=${left},top=${top}`, + ); + }; + + const openStructureDetail = (objId: string) => { + window.open( + `/product/popup/set-structure?objId=${encodeURIComponent(objId)}`, + "setStructurePopup", + "width=1880,height=900,resizable=yes,scrollbars=yes", + ); + }; + + const columns: GridColumn[] = [ + { title: "프로젝트번호", field: "PROJECT_NO", width: 120, hozAlign: "left" }, + { title: "고객사", field: "CUSTOMER_NAME", width: 140, hozAlign: "left" }, + { title: "고객사프로젝트명", field: "CUSTOMER_PROJECT_NAME", width: 200, hozAlign: "left" }, + { title: "유닛명", field: "UNIT_NAME", width: 270, hozAlign: "left" }, + { + title: "E-BOM", field: "BOM_CNT", width: 80, hozAlign: "center", + formatter: (cell) => , + cellClick: (row) => openStructureDetail(String(row.OBJID)), + }, + { title: "등록자", field: "DEPT_USER_NAME", width: 120, hozAlign: "center" }, + { title: "등록일", field: "REG_DATE", width: 100, hozAlign: "center" }, + { title: "배포일", field: "DEPLOY_DATE", width: 100, hozAlign: "center" }, + { title: "Version", field: "REVISION", width: 85, hozAlign: "center" }, + { title: "배포사유", field: "NOTE", hozAlign: "left" }, + { title: "상태", field: "STATUS_TITLE", width: 100, hozAlign: "center" }, + ]; + + const fetchData = useCallback(async () => { + setLoading(true); + try { + const res = await fetch("/api/product/bom", { + method: "POST", headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + mode: "structure", + customer_cd: customerCd, + project_name: projectName, + unit_code: unitCode, + SEARCH_UNIT_NAME: searchUnitName, + SEARCH_WRITER: searchWriter, + SEARCH_DEPLOY_DATE_FROM: deployFrom, + SEARCH_DEPLOY_DATE_TO: deployTo, + status, + }), + }); + if (res.ok) { + const j = await res.json(); + setData(j.RESULTLIST || []); + } + } finally { + setLoading(false); + } + }, [customerCd, projectName, unitCode, searchUnitName, searchWriter, deployFrom, deployTo, status]); + + useEffect(() => { fetchData(); }, [fetchData]); + + const handleDeploy = async () => { + if (selectedRows.length === 0) { Swal.fire("알림", "선택된 내용이 없습니다.", "warning"); return; } + const isDeployed = selectedRows.some((r) => r.STATUS === "deploy"); + if (isDeployed) { Swal.fire("알림", "배포완료건은 배포 할 수 없습니다.", "warning"); return; } + + // 동시배포 체크 (MULTI_MASTER_OBJID 동일해야 동시배포 가능) + if (selectedRows.length > 1) { + let prevMaster = ""; + let onlyMulti = true; + for (const r of selectedRows) { + const master = String(r.MULTI_MASTER_OBJID || r.OBJID); + if (!prevMaster || master === prevMaster) prevMaster = master; + else { onlyMulti = false; break; } + } + if (!onlyMulti) { + Swal.fire("알림", "한번에 한개의 배포만 가능합니다.(동시 프로젝트 등록중인 건만 동시배포 가능합니다.)", "warning"); + return; + } + } + + const c = await Swal.fire({ + title: "선택된 내용을 배포하시겠습니까?", icon: "warning", + showCancelButton: true, confirmButtonText: "확인", cancelButtonText: "취소", + }); + if (!c.isConfirmed) return; + openChangeDesignNote(selectedRows.map((r) => String(r.OBJID)).join(",")); + }; + + const handleDelete = async () => { + if (selectedRows.length === 0) { Swal.fire("알림", "선택된 내용이 없습니다.", "warning"); return; } + const blocked = selectedRows.some((r) => r.STATUS === "deploy" || r.STATUS === "changeDesign"); + if (blocked) { Swal.fire("알림", "배포완료/설계변경미배포 건은 삭제 할 수 없습니다.", "warning"); return; } + + const c = await Swal.fire({ + title: "선택한 정보를 삭제하시겠습니까?", icon: "warning", + showCancelButton: true, confirmButtonText: "확인", cancelButtonText: "취소", + }); + if (!c.isConfirmed) return; + + const res = await fetch("/api/product/bom/structure-delete", { + method: "POST", headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ objIds: selectedRows.map((r) => String(r.OBJID)) }), + }); + const j = await res.json(); + if (j.success) { + await Swal.fire({ icon: "success", title: j.message, timer: 1200, showConfirmButton: false }); + fetchData(); + } else { + Swal.fire("오류", j.message, "error"); + } + }; + + const handleExcelDownload = () => { + if (data.length === 0) { + Swal.fire("알림", "다운로드할 데이터가 없습니다.", "warning"); + return; + } + const header = [ + "프로젝트번호", "고객사", "고객사프로젝트명", "유닛명", "E-BOM", + "등록자", "등록일", "배포일", "Version", "배포사유", "상태", + ]; + const body = data.map((r) => [ + r.PROJECT_NO ?? "", r.CUSTOMER_NAME ?? "", r.CUSTOMER_PROJECT_NAME ?? "", + r.UNIT_NAME ?? "", Number(r.BOM_CNT ?? 0), r.DEPT_USER_NAME ?? "", + r.REG_DATE ?? "", r.DEPLOY_DATE ?? "", r.REVISION ?? "", + r.NOTE ?? "", r.STATUS_TITLE ?? "", + ]); + const ws = XLSX.utils.aoa_to_sheet([header, ...body]); + const wb = XLSX.utils.book_new(); + XLSX.utils.book_append_sheet(wb, ws, "PART 및 구조등록"); + const today = new Date().toISOString().slice(0, 10); + XLSX.writeFile(wb, `PART_및_구조등록_${today}.xlsx`); + }; + + const handleSaveExcel = () => { + if (selectedRows.length > 1) { Swal.fire("알림", "단건만 등록 가능합니다.", "warning"); return; } + if (selectedRows.length === 1) { + const r = selectedRows[0]; + if (String(r.STATUS || "") !== "create") { + Swal.fire("알림", "등록중인 건만 등록/추가 할 수 있습니다.", "warning"); + return; + } + const qs = new URLSearchParams({ + customer_cd: String(r.CUSTOMER_OBJID || ""), + project_name: String(r.CONTRACT_OBJID || ""), + unit_code: String(r.UNIT_CODE || ""), + BOM_REPORT_OBJID: String(r.OBJID || ""), + }).toString(); + window.open(`/product/popup/bom-excel-import?${qs}`, "openBomReportExcelImportPopUp", + "width=1920,height=860,resizable=yes,scrollbars=yes"); + return; + } + if (!customerCd) { Swal.fire("알림", "고객사를 선택해 주세요.", "warning"); return; } + if (!projectName) { Swal.fire("알림", "프로젝트를 선택해 주세요.", "warning"); return; } + if (!unitCode) { Swal.fire("알림", "유닛명을 선택해 주세요.", "warning"); return; } + const qs = new URLSearchParams({ + customer_cd: customerCd, project_name: projectName, unit_code: unitCode, + }).toString(); + window.open(`/product/popup/bom-excel-import?${qs}`, "openBomReportExcelImportPopUp", + "width=1920,height=860,resizable=yes,scrollbars=yes"); + }; + + return ( +
+
+

제품관리_PART 및 구조등록

+
+ + + + + + +
+
+ + + + + + + + + + + + + setSearchUnitName(e.target.value)} className="w-[140px]" /> + + + + + +
+ setDeployFrom(e.target.value)} className="w-[140px]" /> + ~ + setDeployTo(e.target.value)} className="w-[140px]" /> +
+
+ + + +
+ + +
+ ); +} diff --git a/src/app/(main)/product/design-change/page.tsx b/src/app/(main)/product/design-change/page.tsx new file mode 100644 index 0000000..ef8758e --- /dev/null +++ b/src/app/(main)/product/design-change/page.tsx @@ -0,0 +1,196 @@ +"use client"; + +import { useState, useCallback, useEffect } from "react"; +import { DataGrid, type GridColumn } from "@/components/grid/data-grid"; +import { SearchForm, SearchField } from "@/components/layout/search-form"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { SearchableCodeSelect } from "@/components/ui/searchable-code-select"; +import { SearchableSelect } from "@/components/ui/searchable-select"; +import { ExcelDownloadButton } from "@/components/ui/excel-download-button"; +import Swal from "sweetalert2"; + +// 제품관리_설계변경 리스트 (원본: partMng/partMngHisList.jsp) +export default function DesignChangePage() { + const currentYear = new Date().getFullYear(); + const [year, setYear] = useState(""); + const [contractObjid, setContractObjid] = useState(""); + const [unitCode, setUnitCode] = useState(""); + const [partNo, setPartNo] = useState(""); + const [partName, setPartName] = useState(""); + const [changeOption, setChangeOption] = useState(""); + const [eoStart, setEoStart] = useState(""); + const [eoEnd, setEoEnd] = useState(""); + const [changeType, setChangeType] = useState(""); + const [partType, setPartType] = useState(""); + const [writerId, setWriterId] = useState(""); + + const [data, setData] = useState[]>([]); + const [loading, setLoading] = useState(false); + const [projects, setProjects] = useState<{ value: string; label: string }[]>([]); + const [units, setUnits] = useState<{ value: string; label: string }[]>([]); + const [users, setUsers] = useState<{ value: string; label: string }[]>([]); + + // 프로젝트 목록 로드 + useEffect(() => { + fetch("/api/common/project-list", { + method: "POST", headers: { "Content-Type": "application/json" }, body: "{}", + }).then((r) => r.json()) + .then((j) => setProjects((j.RESULTLIST || []).map((r: Record) => ({ + value: String(r.OBJID), label: String(r.LABEL || r.PROJECT_NO || r.OBJID), + })))) + .catch(() => {}); + }, []); + + // 프로젝트 변경 시 유닛 목록 로드 + useEffect(() => { + if (!contractObjid) { setUnits([]); return; } + fetch("/api/common/unit-list", { + method: "POST", headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ contract_objid: contractObjid }), + }).then((r) => r.json()) + .then((j) => setUnits((j.RESULTLIST || []).map((r: Record) => ({ + value: String(r.OBJID), label: String(r.UNIT_NAME || r.TASK_NAME), + })))) + .catch(() => {}); + setUnitCode(""); + }, [contractObjid]); + + // 사용자 목록 로드 + useEffect(() => { + fetch("/api/admin/users", { method: "POST", headers: { "Content-Type": "application/json" }, body: "{}" }) + .then((r) => r.json()) + .then((j) => setUsers((j.RESULTLIST || []).map((r: Record) => ({ + value: String(r.USER_ID), label: String(r.USER_NAME || ""), + })))) + .catch(() => {}); + }, []); + + const openHisDetail = (objId: string) => { + const w = 800, h = 550; + const left = (window.screen.width - w) / 2; + const top = (window.screen.height - h) / 2; + window.open( + `/product/popup/part-his-detail?objId=${encodeURIComponent(objId)}`, + "partMngHisDetailPopUp", + `width=${w},height=${h},left=${left},top=${top}`, + ); + }; + + const columns: GridColumn[] = [ + { title: "EO No", field: "EO_NO", width: 85, hozAlign: "left" }, + { title: "프로젝트번호", field: "PROJECT_NO", width: 100, hozAlign: "left" }, + { title: "프로젝트명", field: "PROJECT_NAME", width: 150, hozAlign: "left" }, + { title: "유닛명", field: "UNIT_NAME", width: 150, hozAlign: "left" }, + { title: "모품번", field: "PARENT_PART_INFO", width: 150, hozAlign: "left" }, + { + title: "품번", field: "PART_NO", width: 150, hozAlign: "left", + cellClick: (row) => openHisDetail(String(row.OBJID)), + }, + { title: "품명", field: "PART_NAME", hozAlign: "left" }, + { title: "수량", field: "QTY", width: 55, hozAlign: "center" }, + { title: "변경수량", field: "QTY_TEMP", width: 75, hozAlign: "center" }, + { title: "EO구분", field: "CHANGE_TYPE_NAME", width: 75, hozAlign: "center" }, + { title: "EO사유", field: "CHANGE_OPTION_NAME", width: 75, hozAlign: "center" }, + { title: "Revision", field: "REVISION", width: 85, hozAlign: "center" }, + { title: "EO Date", field: "EO_DATE", width: 85, hozAlign: "center" }, + { title: "PART구분", field: "PART_TYPE_NAME", width: 90, hozAlign: "center" }, + { title: "담당자", field: "WRITER_NAME", width: 80, hozAlign: "center" }, + { title: "실행일", field: "HIS_REG_DATE_TITLE", width: 80, hozAlign: "center" }, + ]; + + const fetchData = useCallback(async () => { + setLoading(true); + try { + const res = await fetch("/api/product/design-change", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + Year: year, + contract_objid: contractObjid, + unit_code: unitCode, + part_no: partNo, + part_name: partName, + change_option: changeOption, + eo_start_date: eoStart, + eo_end_date: eoEnd, + change_type: changeType, + part_type: partType, + writer_id: writerId, + }), + }); + if (res.ok) { + const json = await res.json(); + setData(json.RESULTLIST || []); + } + } catch { + Swal.fire("오류", "데이터 조회 중 오류가 발생했습니다.", "error"); + } finally { + setLoading(false); + } + }, [year, contractObjid, unitCode, partNo, partName, changeOption, eoStart, eoEnd, changeType, partType, writerId]); + + useEffect(() => { fetchData(); }, [fetchData]); + + return ( +
+
+

제품관리_설계변경 리스트

+
+ + + +
+
+ + + + + + + + + + + + + setPartNo(e.target.value)} className="w-[140px]" /> + + + setPartName(e.target.value)} className="w-[140px]" /> + + + + + +
+ setEoStart(e.target.value)} className="w-[140px]" /> + ~ + setEoEnd(e.target.value)} className="w-[140px]" /> +
+
+ + + + + + + + + +
+ + +
+ ); +} diff --git a/src/app/(main)/product/part-change/page.tsx b/src/app/(main)/product/part-change/page.tsx new file mode 100644 index 0000000..1a15de6 --- /dev/null +++ b/src/app/(main)/product/part-change/page.tsx @@ -0,0 +1,420 @@ +"use client"; + +import { useState, useCallback, useEffect, useMemo } from "react"; +import { SearchForm, SearchField } from "@/components/layout/search-form"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { SearchableSelect } from "@/components/ui/searchable-select"; +import { FolderCell } from "@/components/ui/folder-cell"; +import { ExcelDownloadButton } from "@/components/ui/excel-download-button"; +import type { ExcelColumn } from "@/lib/excel-export"; +import Swal from "sweetalert2"; + +type Row = Record; +type CodeOpt = { CODE_ID: string; CODE_NAME: string }; + +// 제품관리_설변대상 PART조회 (원본: partMng/partMngChangeList.jsp) +export default function PartChangePage() { + const [projectName, setProjectName] = useState(""); + const [unitCode, setUnitCode] = useState(""); + const [searchPartObjid, setSearchPartObjid] = useState(""); + + const [data, setData] = useState([]); + const [loading, setLoading] = useState(false); + const [projects, setProjects] = useState<{ value: string; label: string }[]>([]); + const [units, setUnits] = useState<{ value: string; label: string }[]>([]); + const [parts, setParts] = useState<{ value: string; label: string }[]>([]); + + const [partTypes, setPartTypes] = useState([]); + const [changeTypes, setChangeTypes] = useState([]); + const [changeOptions, setChangeOptions] = useState([]); + + // 공통코드 로드 (API 응답이 소문자 키라 대문자로 정규화) + useEffect(() => { + const loadCode = async (codeId: string): Promise => { + const res = await fetch("/api/common/code-list", { + method: "POST", headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ codeId }), + }); + const j = await res.json(); + return (j.data || []).map((r: Record) => ({ + CODE_ID: String(r.code_id || r.CODE_ID || ""), + CODE_NAME: String(r.code_name || r.CODE_NAME || ""), + })); + }; + loadCode("0000062").then(setPartTypes); + loadCode("0001054").then(setChangeTypes); // PART_CHANGE_TYPE_CODE + loadCode("0000318").then(setChangeOptions); // PART_CHANGE_OPTION_CODE + }, []); + + // 프로젝트 목록 + useEffect(() => { + fetch("/api/common/project-list", { + method: "POST", headers: { "Content-Type": "application/json" }, body: "{}", + }).then((r) => r.json()) + .then((j) => setProjects((j.RESULTLIST || []).map((r: Row) => ({ + value: String(r.OBJID), label: String(r.LABEL || r.PROJECT_NO || r.OBJID), + })))) + .catch(() => {}); + }, []); + + // 프로젝트 변경 시 유닛 + useEffect(() => { + setUnitCode(""); + setSearchPartObjid(""); + if (!projectName) { setUnits([]); return; } + fetch("/api/common/unit-list", { + method: "POST", headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ contract_objid: projectName }), + }).then((r) => r.json()) + .then((j) => setUnits((j.RESULTLIST || []).map((r: Row) => ({ + value: String(r.OBJID), label: String(r.UNIT_NAME || r.TASK_NAME), + })))) + .catch(() => {}); + }, [projectName]); + + // 유닛 변경 시 PART 목록 + useEffect(() => { + setSearchPartObjid(""); + if (!projectName || !unitCode) { setParts([]); return; } + fetch("/api/product/part-change", { + method: "POST", headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ action: "list", project_name: projectName, unit_code: unitCode }), + }).then((r) => r.json()) + .then((j) => { + const uniq = new Map(); + (j.RESULTLIST || []).forEach((r: Row) => { + const id = String(r.OBJID); + uniq.set(id, `${r.PART_NO ?? ""} ${r.PART_NAME ?? ""}`); + }); + setParts([...uniq.entries()].map(([value, label]) => ({ value, label }))); + }) + .catch(() => {}); + }, [projectName, unitCode]); + + const fetchData = useCallback(async () => { + if (!projectName) { + Swal.fire("알림", "프로젝트번호를 선택하세요.", "warning"); + return; + } + if (!unitCode) { + Swal.fire("알림", "유닛명을 선택하세요.", "warning"); + return; + } + if (!searchPartObjid) { + Swal.fire("알림", "품번을 선택하세요.", "warning"); + return; + } + setLoading(true); + try { + const res = await fetch("/api/product/part-change", { + method: "POST", headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + action: "list", + project_name: projectName, + unit_code: unitCode, + SEARCH_PART_OBJID: searchPartObjid, + }), + }); + if (res.ok) { + const j = await res.json(); + setData(j.RESULTLIST || []); + } + } finally { + setLoading(false); + } + }, [projectName, unitCode, searchPartObjid]); + + const updateCell = (idx: number, field: string, value: string) => { + setData((prev) => { + const next = [...prev]; + next[idx] = { ...next[idx], [field]: value }; + // 같은 품번끼리 동기화 (원본 cellEdited 동작) + const partNo = next[idx].PART_NO; + for (let i = 0; i < next.length; i++) { + if (i !== idx && next[i].PART_NO === partNo) { + next[i] = { ...next[i], [field]: value }; + } + } + return next; + }); + }; + + // 같은 PART_NO가 여러 행(동시 적용 BOM)으로 올 때 첫 번째만 편집 가능. + // updateCell 이 같은 PART_NO 나머지 행도 동기화하므로 하나만 활성화해도 충분. + const editableIdxSet = useMemo(() => { + const seen = new Set(); + const s = new Set(); + data.forEach((row, idx) => { + if (String(row.Q_STATUS || "") !== "deploy") return; + const partNo = String(row.PART_NO ?? ""); + if (!seen.has(partNo)) { + seen.add(partNo); + s.add(idx); + } + }); + return s; + }, [data]); + + const isEditable = (idx: number) => editableIdxSet.has(idx); + + const handleSave = async () => { + if (data.length === 0) { + Swal.fire("알림", "저장할 데이터가 없습니다.", "warning"); + return; + } + const allDeploy = data.every((r) => String(r.Q_STATUS || "") === "deploy"); + if (!allDeploy) { + Swal.fire("알림", "설변중인 파트입니다. 배포후에 다시 설계변경 할 수 있습니다.", "warning"); + return; + } + const allFilled = data.every((r) => + String(r.CHANGE_TYPE || "") !== "" && String(r.CHANGE_OPTION || "") !== "" + ); + if (!allFilled) { + Swal.fire("알림", "EO구분, EO사유는 필수 입력입니다.", "warning"); + return; + } + const c = await Swal.fire({ + title: "저장하시겠습니까?", icon: "question", showCancelButton: true, + confirmButtonText: "확인", cancelButtonText: "취소", + }); + if (!c.isConfirmed) return; + const res = await fetch("/api/product/part-change", { + method: "POST", headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ action: "save", rows: data }), + }); + const j = await res.json(); + if (j.success) { + await Swal.fire({ icon: "success", title: j.message, timer: 1200, showConfirmButton: false }); + fetchData(); + } else { + Swal.fire("오류", j.message || "저장 실패", "error"); + } + }; + + const openPartForm = (objId: string) => { + const w = 600, h = 500; + const left = (window.screen.width - w) / 2; + const top = (window.screen.height - h) / 2; + window.open(`/product/popup/part-form?objId=${encodeURIComponent(objId)}`, "partMngPopUp", + `width=${w},height=${h},left=${left},top=${top}`); + }; + + const openFilePopup = (objId: string, docType: string, docTypeName: string) => { + const w = 800, h = 335; + const left = (window.screen.width - w) / 2; + const top = (window.screen.height - h) / 2; + window.open( + `/common/files?targetObjId=${encodeURIComponent(objId)}&docType=${encodeURIComponent(docType)}&docTypeName=${encodeURIComponent(docTypeName)}`, + "fileRegistPopUp", `width=${w},height=${h},left=${left},top=${top}`, + ); + }; + + const yellowHeader = "px-2 py-1 text-[#FFBB00] border border-gray-200 font-semibold text-center whitespace-nowrap"; + const normalHeader = "px-2 py-1 border border-gray-200 font-semibold text-center whitespace-nowrap"; + // frozen 셀/헤더 공통 스타일 — 인라인 스타일로 sticky/bg/zIndex 강제 적용 (border-collapse:separate 와 조합) + const freezeHeader = (left: number): React.CSSProperties => ({ + position: "sticky", left, top: 0, zIndex: 30, backgroundColor: "#F9FAFB", + }); + const freezeCell = (left: number): React.CSSProperties => ({ + position: "sticky", left, zIndex: 20, backgroundColor: "#FFFFFF", + }); + + // Excel 컬럼 정의 (원본 partMngChangeList 헤더 순서 그대로) + const excelColumns: ExcelColumn[] = useMemo(() => { + const codeName = (list: CodeOpt[], id: string) => + list.find((c) => c.CODE_ID === id)?.CODE_NAME ?? id; + return [ + { title: "프로젝트번호", field: "PROJECT_NO" }, + { title: "유닛명", field: "UNIT_NAME" }, + { title: "모품번", field: "PARENT_PART_INFO" }, + { title: "품번", field: "PART_NO" }, + { title: "품명", field: "PART_NAME" }, + { title: "수량", field: "Q_QTY" }, + { title: "3D", excelFormatter: (r) => Number(r.CU01_CNT ?? 0) }, + { title: "2D", excelFormatter: (r) => Number(r.CU02_CNT ?? 0) }, + { title: "PDF", excelFormatter: (r) => Number(r.CU03_CNT ?? 0) }, + { title: "EO구분", excelFormatter: (r) => codeName(changeTypes, String(r.CHANGE_TYPE ?? "")) }, + { title: "EO사유", excelFormatter: (r) => codeName(changeOptions, String(r.CHANGE_OPTION ?? "")) }, + { title: "PART구분", excelFormatter: (r) => codeName(partTypes, String(r.PART_TYPE ?? "")) }, + { title: "재질", field: "MATERIAL" }, + { title: "사양(규격)", field: "SPEC" }, + { title: "후처리", field: "POST_PROCESSING" }, + { title: "MAKER", field: "MAKER" }, + { title: "대분류", field: "MAJOR_CATEGORY" }, + { title: "중분류", field: "SUB_CATEGORY" }, + { title: "Revision", field: "REVISION" }, + { title: "EO No", field: "EO_NO" }, + { title: "EO Date", field: "EO_DATE" }, + { title: "비고", field: "REMARK" }, + ]; + }, [changeTypes, changeOptions, partTypes]); + + const codeSel = useMemo(() => (opts: CodeOpt[], val: string, onChange: (v: string) => void, disabled: boolean) => ( + + ), []); + + return ( +
+
+

제품관리_설변대상 PART조회

+
+ + + + +
+
+ + + + + + + + + + + + + +
+ {loading && ( +
+
+
+ Loading... +
+
+ )} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {data.length === 0 ? ( + + ) : data.map((row, idx) => { + const editable = isEditable(idx); + const frozenCell = "px-2 py-1 border border-gray-200"; + return ( + + + + + + + + + + + + + + + + + + + + + + + + + ); + })} + +
프로젝트번호유닛명모품번품번품명수량3D2DPDFEO구분EO사유PART구분재질사양(규격)후처리MAKER대분류중분류RevisionEO NoEO Date비고
데이터가 없습니다.
{String(row.PROJECT_NO ?? "")}{String(row.UNIT_NAME ?? "")}{String(row.PARENT_PART_INFO ?? "")} + openPartForm(String(row.OBJID))}> + {String(row.PART_NO ?? "")} + + {String(row.PART_NAME ?? "")} + {editable ? ( + updateCell(idx, "Q_QTY", e.target.value)} + className="w-full h-6 text-xs border border-gray-300 rounded px-1 text-right" /> + ) : String(row.Q_QTY ?? "")} + + openFilePopup(String(row.OBJID), "3D_CAD", "3D CAD 첨부파일")} /> + + openFilePopup(String(row.OBJID), "2D_DRAWING_CAD", "2D(Drawing) CAD 첨부파일")} /> + + openFilePopup(String(row.OBJID), "2D_PDF_CAD", "2D(PDF) CAD 첨부파일")} /> + {codeSel(changeTypes, String(row.CHANGE_TYPE ?? ""), (v) => updateCell(idx, "CHANGE_TYPE", v), !editable)}{codeSel(changeOptions, String(row.CHANGE_OPTION ?? ""), (v) => updateCell(idx, "CHANGE_OPTION", v), !editable)}{codeSel(partTypes, String(row.PART_TYPE ?? ""), (v) => updateCell(idx, "PART_TYPE", v), !editable)} + updateCell(idx, "MATERIAL", e.target.value)} disabled={!editable} className="h-6 text-xs" /> + + updateCell(idx, "SPEC", e.target.value)} disabled={!editable} className="h-6 text-xs" /> + + updateCell(idx, "POST_PROCESSING", e.target.value)} disabled={!editable} className="h-6 text-xs" /> + + updateCell(idx, "MAKER", e.target.value)} disabled={!editable} className="h-6 text-xs" /> + + updateCell(idx, "MAJOR_CATEGORY", e.target.value)} disabled={!editable} className="h-6 text-xs" /> + + updateCell(idx, "SUB_CATEGORY", e.target.value)} disabled={!editable} className="h-6 text-xs" /> + {String(row.REVISION ?? "")}{String(row.EO_NO ?? "")}{String(row.EO_DATE ?? "")} + updateCell(idx, "REMARK", e.target.value)} disabled={!editable} className="h-6 text-xs" /> +
+
+
+ ); +} diff --git a/src/app/(main)/product/part-list/page.tsx b/src/app/(main)/product/part-list/page.tsx new file mode 100644 index 0000000..4bd8038 --- /dev/null +++ b/src/app/(main)/product/part-list/page.tsx @@ -0,0 +1,182 @@ +"use client"; + +import { useState, useCallback, useEffect } from "react"; +import { DataGrid, type GridColumn } from "@/components/grid/data-grid"; +import { SearchForm, SearchField } from "@/components/layout/search-form"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { SearchableCodeSelect } from "@/components/ui/searchable-code-select"; +import { FolderCell } from "@/components/ui/folder-cell"; +import * as XLSX from "xlsx"; +import Swal from "sweetalert2"; + +// 제품관리_PART 조회 (원본: partMng/partMngList.jsp) +export default function PartListPage() { + const [searchPartNo, setSearchPartNo] = useState(""); + const [searchPartName, setSearchPartName] = useState(""); + const [searchRevision, setSearchRevision] = useState("0"); // 원본 JSP 기본값 = all + const [searchMaterial, setSearchMaterial] = useState(""); + const [searchSpec, setSearchSpec] = useState(""); + const [searchPartType, setSearchPartType] = useState(""); + const [data, setData] = useState[]>([]); + const [loading, setLoading] = useState(false); + + const centerPopup = (url: string, name: string, w: number, h: number) => { + const left = (window.screen.width - w) / 2; + const top = (window.screen.height - h) / 2; + window.open(url, name, `width=${w},height=${h},left=${left},top=${top},menubars=no,scrollbars=yes,resizable=yes`); + }; + + const openPartDetail = (objId: string) => { + centerPopup(`/product/popup/part-form?objId=${encodeURIComponent(objId)}`, "partMngPopUp", 900, 600); + }; + + const openFilePopup = (objId: string, docType: string, docTypeName: string) => { + centerPopup( + `/common/files?targetObjId=${encodeURIComponent(objId)}&docType=${encodeURIComponent(docType)}&docTypeName=${encodeURIComponent(docTypeName)}`, + "fileRegistPopUp", + 800, + 335, + ); + }; + + const columns: GridColumn[] = [ + { + title: "품번", field: "PART_NO", width: 125, hozAlign: "left", + cellClick: (row) => openPartDetail(String(row.OBJID)), + }, + { + title: "품명", field: "PART_NAME", hozAlign: "left", + cellClick: (row) => openPartDetail(String(row.OBJID)), + }, + { title: "수량", field: "BOM_QTY", width: 50, hozAlign: "center" }, + { + title: "3D", field: "CU01_CNT", width: 45, hozAlign: "center", + formatter: (cell) => , + cellClick: (row) => openFilePopup(String(row.OBJID), "3D_CAD", "3D CAD 첨부파일"), + }, + { + title: "2D", field: "CU02_CNT", width: 45, hozAlign: "center", + formatter: (cell) => , + cellClick: (row) => openFilePopup(String(row.OBJID), "2D_DRAWING_CAD", "2D(Drawing) CAD 첨부파일"), + }, + { + title: "PDF", field: "CU03_CNT", width: 55, hozAlign: "center", + formatter: (cell) => , + cellClick: (row) => openFilePopup(String(row.OBJID), "2D_PDF_CAD", "2D(PDF) CAD 첨부파일"), + }, + { title: "재질", field: "MATERIAL", width: 90, hozAlign: "left" }, + { title: "사양(규격)", field: "SPEC", width: 90, hozAlign: "left" }, + { title: "후처리", field: "POST_PROCESSING", width: 80, hozAlign: "left" }, + { title: "MAKER", field: "MAKER", width: 80, hozAlign: "left" }, + { title: "대분류", field: "MAJOR_CATEGORY", width: 80, hozAlign: "left" }, + { title: "중분류", field: "SUB_CATEGORY", width: 80, hozAlign: "left" }, + { title: "Revision", field: "REVISION", width: 80, hozAlign: "center" }, + { title: "EO No", field: "EO_NO", width: 90, hozAlign: "center" }, + { title: "EO Date", field: "EO_DATE", width: 80, hozAlign: "center" }, + { title: "PART구분", field: "PART_TYPE_TITLE", width: 88, hozAlign: "center" }, + { title: "비고", field: "REMARK", width: 80, hozAlign: "left" }, + ]; + + const fetchData = useCallback(async () => { + setLoading(true); + try { + const res = await fetch("/api/product/part", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + mode: "list", + SEARCH_PART_NO: searchPartNo, + SEARCH_PART_NAME: searchPartName, + SEARCH_REVISION_RELEASE: searchRevision, + SEARCH_MATERIAL: searchMaterial, + SEARCH_SPEC: searchSpec, + SEARCH_PART_TYPE: searchPartType, + }), + }); + if (res.ok) { + const json = await res.json(); + setData(json.RESULTLIST || []); + } + } catch { + Swal.fire("오류", "데이터 조회 중 오류가 발생했습니다.", "error"); + } finally { + setLoading(false); + } + }, [searchPartNo, searchPartName, searchRevision, searchMaterial, searchSpec, searchPartType]); + + useEffect(() => { fetchData(); }, [fetchData]); + + const handleExcel = () => { + if (data.length === 0) { + Swal.fire("알림", "다운로드할 데이터가 없습니다.", "warning"); + return; + } + const header = [ + "품번", "품명", "수량", "3D", "2D", "PDF", "재질", "사양(규격)", + "후처리", "MAKER", "대분류", "중분류", "Revision", "EO No", + "EO Date", "PART구분", "비고", + ]; + const body = data.map((r) => [ + r.PART_NO ?? "", r.PART_NAME ?? "", r.BOM_QTY ?? "", + r.CU01_CNT ?? 0, r.CU02_CNT ?? 0, r.CU03_CNT ?? 0, + r.MATERIAL ?? "", r.SPEC ?? "", r.POST_PROCESSING ?? "", + r.MAKER ?? "", r.MAJOR_CATEGORY ?? "", r.SUB_CATEGORY ?? "", + r.REVISION ?? "", r.EO_NO ?? "", r.EO_DATE ?? "", + r.PART_TYPE_TITLE ?? "", r.REMARK ?? "", + ]); + const ws = XLSX.utils.aoa_to_sheet([header, ...body]); + const wb = XLSX.utils.book_new(); + XLSX.utils.book_append_sheet(wb, ws, "PART 조회"); + const today = new Date().toISOString().slice(0, 10); + XLSX.writeFile(wb, `PART_조회_${today}.xlsx`); + }; + + const handleReset = () => { + setSearchPartNo(""); setSearchPartName(""); setSearchRevision("0"); + setSearchMaterial(""); setSearchSpec(""); setSearchPartType(""); + }; + + return ( +
+
+

제품관리_PART 조회

+
+ + + +
+
+ + + + setSearchPartNo(e.target.value)} className="w-[194px]" /> + + + setSearchPartName(e.target.value)} className="w-[150px]" /> + + + + + + setSearchMaterial(e.target.value)} className="w-[150px]" /> + + + setSearchSpec(e.target.value)} className="w-[150px]" /> + + + + + + + +
+ ); +} diff --git a/src/app/(main)/product/part-register/page.tsx b/src/app/(main)/product/part-register/page.tsx new file mode 100644 index 0000000..7bb9934 --- /dev/null +++ b/src/app/(main)/product/part-register/page.tsx @@ -0,0 +1,238 @@ +"use client"; + +import { useState, useCallback, useEffect } from "react"; +import { DataGrid, type GridColumn } from "@/components/grid/data-grid"; +import { SearchForm, SearchField } from "@/components/layout/search-form"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { SearchableSelect } from "@/components/ui/searchable-select"; +import { FolderCell } from "@/components/ui/folder-cell"; +import Swal from "sweetalert2"; + +// 제품관리_PART 등록 (원본: partMng/partMngTempList.jsp) +export default function PartRegisterPage() { + const currentYear = new Date().getFullYear(); + const [searchYear, setSearchYear] = useState(""); + const [searchPartNo, setSearchPartNo] = useState(""); + const [searchPartName, setSearchPartName] = useState(""); + const [writer, setWriter] = useState(""); + const [data, setData] = useState[]>([]); + const [loading, setLoading] = useState(false); + const [selectedRows, setSelectedRows] = useState[]>([]); + const [users, setUsers] = useState<{ value: string; label: string }[]>([]); + + useEffect(() => { + fetch("/api/admin/users", { method: "POST", headers: { "Content-Type": "application/json" }, body: "{}" }) + .then((r) => r.json()) + .then((d) => setUsers((d.RESULTLIST || []).map((r: Record) => ({ + value: String(r.USER_ID), label: String(r.USER_NAME || ""), + })))) + .catch(() => {}); + }, []); + + const centerPopup = (url: string, name: string, w: number, h: number) => { + const left = (window.screen.width - w) / 2; + const top = (window.screen.height - h) / 2; + window.open(url, name, `width=${w},height=${h},left=${left},top=${top},menubars=no,scrollbars=yes,resizable=yes`); + }; + + const openPartForm = (objId?: string) => { + const url = objId + ? `/product/popup/part-form?objId=${encodeURIComponent(objId)}` + : `/product/popup/part-form`; + centerPopup(url, "partMngPopUp", 900, 600); + }; + + const openFileRegist = (objId: string, docType: string, docTypeName: string) => { + centerPopup( + `/common/files?targetObjId=${encodeURIComponent(objId)}&docType=${encodeURIComponent(docType)}&docTypeName=${encodeURIComponent(docTypeName)}`, + "fileRegistPopUp", + 800, + 300, + ); + }; + + const columns: GridColumn[] = [ + { title: "순", field: "RNUM", width: 50, hozAlign: "center" }, + { title: "품명", field: "PART_NAME", width: 240, hozAlign: "left", frozen: true }, + { title: "모품번", field: "PARENT_PART_INFO", width: 120, hozAlign: "left", frozen: true }, + { + title: "품번", field: "PART_NO", width: 160, hozAlign: "left", frozen: true, + cellClick: (row) => openPartForm(String(row.OBJID)), + }, + { title: "수량", field: "Q_QTY", width: 70, hozAlign: "right" }, + { + title: "3D", field: "CU01_CNT", width: 60, hozAlign: "center", + formatter: (cell) => , + cellClick: (row) => openFileRegist(String(row.OBJID), "3D_CAD", "3D CAD 첨부파일"), + }, + { + title: "2D", field: "CU02_CNT", width: 60, hozAlign: "center", + formatter: (cell) => , + cellClick: (row) => openFileRegist(String(row.OBJID), "2D_DRAWING_CAD", "2D(Drawing) CAD 첨부파일"), + }, + { + title: "PDF", field: "CU03_CNT", width: 60, hozAlign: "center", + formatter: (cell) => , + cellClick: (row) => openFileRegist(String(row.OBJID), "2D_PDF_CAD", "2D(PDF) CAD 첨부파일"), + }, + { title: "재질", field: "MATERIAL", width: 100, hozAlign: "left" }, + { title: "사양(규격)", field: "SPEC", width: 180, hozAlign: "left" }, + { title: "후처리", field: "POST_PROCESSING", width: 90, hozAlign: "left" }, + { title: "MAKER", field: "MAKER", width: 90, hozAlign: "left" }, + { title: "대분류", field: "MAJOR_CATEGORY", width: 110, hozAlign: "left" }, + { title: "중분류", field: "SUB_CATEGORY", width: 110, hozAlign: "left" }, + { title: "Revision", field: "REVISION", width: 80, hozAlign: "center" }, + { title: "EO No", field: "EO_NO", width: 90, hozAlign: "center" }, + { title: "EO Date", field: "EO_DATE", width: 100, hozAlign: "center" }, + { title: "PART 구분", field: "PART_TYPE_TITLE", width: 100, hozAlign: "center" }, + { title: "비고", field: "REMARK", width: 120, hozAlign: "left" }, + ]; + + const fetchData = useCallback(async () => { + setLoading(true); + try { + const res = await fetch("/api/product/part", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + mode: "register", + SEARCH_YEAR: searchYear, + SEARCH_PART_NO: searchPartNo, + SEARCH_PART_NAME: searchPartName, + WRITER: writer, + }), + }); + if (res.ok) { + const json = await res.json(); + setData(json.RESULTLIST || []); + setSelectedRows([]); + } + } catch { + Swal.fire("오류", "데이터 조회 중 오류가 발생했습니다.", "error"); + } finally { + setLoading(false); + } + }, [searchYear, searchPartNo, searchPartName, writer]); + + useEffect(() => { fetchData(); }, [fetchData]); + + const handleDeploy = async () => { + if (selectedRows.length === 0) { + Swal.fire("알림", "선택된 Part가 없습니다.", "warning"); + return; + } + const r = await Swal.fire({ + title: "같은 Part는 동시 확정됩니다. 선택된 Part를 확정하시겠습니까?", + icon: "warning", + showCancelButton: true, + confirmButtonColor: "#3085d6", + cancelButtonColor: "#d33", + confirmButtonText: "확인", + cancelButtonText: "취소", + }); + if (!r.isConfirmed) return; + + const res = await fetch("/api/product/part/deploy", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ rows: selectedRows }), + }); + const json = await res.json(); + if (json.success) { + await Swal.fire({ icon: "success", title: json.message, timer: 1200, showConfirmButton: false }); + fetchData(); + } else { + Swal.fire("오류", json.message, "error"); + } + }; + + const handleDelete = async () => { + if (selectedRows.length === 0) { + Swal.fire("알림", "선택된 Part가 없습니다.", "warning"); + return; + } + const r = await Swal.fire({ + title: "선택된 Part를 삭제하시겠습니까?", + icon: "warning", + showCancelButton: true, + confirmButtonColor: "#3085d6", + cancelButtonColor: "#d33", + confirmButtonText: "확인", + cancelButtonText: "취소", + }); + if (!r.isConfirmed) return; + + const res = await fetch("/api/product/part/delete", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ objIds: selectedRows.map((row) => String(row.OBJID)) }), + }); + const json = await res.json(); + if (json.success) { + await Swal.fire({ icon: "success", title: json.message, timer: 1200, showConfirmButton: false }); + fetchData(); + } else { + Swal.fire("오류", json.message, "error"); + } + }; + + const handleExcelPopup = () => { + centerPopup( + "/product/popup/part-excel-import", + "openPartExcelImportPopUp", + 1520, + 860, + ); + }; + + const handleReset = () => { + setSearchYear(""); setSearchPartNo(""); setSearchPartName(""); setWriter(""); + }; + + return ( +
+
+

제품관리_PART 등록

+
+ + + + + + +
+
+ + + + + + + setSearchPartNo(e.target.value)} className="w-[180px]" /> + + + setSearchPartName(e.target.value)} className="w-[160px]" /> + + + + + + + +
+ ); +} diff --git a/src/app/(main)/product/spec/page.tsx b/src/app/(main)/product/spec/page.tsx new file mode 100644 index 0000000..2b6b0d8 --- /dev/null +++ b/src/app/(main)/product/spec/page.tsx @@ -0,0 +1,116 @@ +"use client"; + +import { useState, useCallback, useEffect } from "react"; +import { DataGrid, type GridColumn } from "@/components/grid/data-grid"; +import { SearchForm, SearchField } from "@/components/layout/search-form"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import Swal from "sweetalert2"; + +// 사양관리 +export default function ProductSpecPage() { + const [projectNo, setProjectNo] = useState(""); + const [specName, setSpecName] = useState(""); + const [data, setData] = useState[]>([]); + + const columns: GridColumn[] = [ + { + title: "프로젝트번호", field: "PROJECT_NO", width: 130, hozAlign: "center", + cellClick: (row) => { + window.open( + `/product/spec/detail?objId=${row.OBJID}`, + "specDetail", + "width=1000,height=700" + ); + }, + }, + { title: "프로젝트명", field: "PROJECT_NAME", width: 200, hozAlign: "left" }, + { title: "사양명", field: "SPEC_NAME", width: 200, hozAlign: "left" }, + { title: "사양값", field: "SPEC_VALUE", width: 150, hozAlign: "left" }, + { title: "단위", field: "UNIT", width: 80, hozAlign: "center" }, + { title: "비고", field: "REMARK", width: 200, hozAlign: "left" }, + { title: "등록일", field: "REG_DATE", width: 100, hozAlign: "center" }, + { title: "등록자", field: "REG_USER_NAME", width: 100, hozAlign: "center" }, + ]; + + const fetchData = useCallback(async () => { + try { + const res = await fetch("/api/product/spec", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ project_no: projectNo, spec_name: specName }), + }); + if (res.ok) { + const json = await res.json(); + setData(json.RESULTLIST || []); + } + } catch { + Swal.fire("오류", "데이터 조회 중 오류가 발생했습니다.", "error"); + } + }, [projectNo, specName]); + + const handleAdd = async () => { + const { value: form } = await Swal.fire({ + title: "사양 등록", + html: ` +
+ + + + + + + + +
`, + showCancelButton: true, confirmButtonText: "저장", cancelButtonText: "취소", + preConfirm: () => ({ + project_no: (document.getElementById("swal-pn") as HTMLInputElement)?.value, + spec_name: (document.getElementById("swal-sn") as HTMLInputElement)?.value, + spec_value: (document.getElementById("swal-sv") as HTMLInputElement)?.value, + unit: (document.getElementById("swal-un") as HTMLInputElement)?.value, + }), + }); + if (!form || !form.spec_name) return; + const res = await fetch("/api/product/spec/save", { + method: "POST", headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ actionType: "regist", ...form }), + }); + const json = await res.json(); + if (json.success) { + Swal.fire({ icon: "success", title: json.message, timer: 1200, showConfirmButton: false }); + fetchData(); + } else Swal.fire("오류", json.message || "저장 실패", "error"); + }; + + const handleDelete = () => { + Swal.fire("알림", "항목을 선택한 뒤 다시 시도하세요 (리스트 삭제는 별도 구현 예정)", "info"); + }; + + // 페이지 진입 시 자동 로드 + useEffect(() => { fetchData(); }, [fetchData]); + + return ( +
+
+

사양관리

+
+ + + +
+
+ + + + setProjectNo(e.target.value)} className="w-[130px]" /> + + + setSpecName(e.target.value)} className="w-[200px]" /> + + + + +
+ ); +} diff --git a/src/app/(main)/production/inspection/page.tsx b/src/app/(main)/production/inspection/page.tsx new file mode 100644 index 0000000..8c57cc8 --- /dev/null +++ b/src/app/(main)/production/inspection/page.tsx @@ -0,0 +1,138 @@ +"use client"; + +import { useState, useCallback, useEffect } from "react"; +import Swal from "sweetalert2"; +import { DataGrid, type GridColumn } from "@/components/grid/data-grid"; +import { SearchForm, SearchField } from "@/components/layout/search-form"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { CodeSelect } from "@/components/ui/code-select"; +import { FolderCell } from "@/components/ui/folder-cell"; + +// productionplanning/inspectionMgmtList.jsp 대응 - 생산관리_검사관리 +function openPopup(url: string, name: string, w: number, h: number) { + const left = (window.screen.width - w) / 2; + const top = (window.screen.height - h) / 2; + window.open(url, name, `width=${w},height=${h},left=${left},top=${top},scrollbars=yes,resizable=yes`); +} + +export default function ProductionInspectionPage() { + const [year, setYear] = useState(new Date().getFullYear().toString()); + const [projectNo, setProjectNo] = useState(""); + const [customerObjid, setCustomerObjid] = useState(""); + const [product, setProduct] = useState(""); + const [pmUserId, setPmUserId] = useState(""); + const [data, setData] = useState[]>([]); + const [selected, setSelected] = useState[]>([]); + + const openProjectForm = (objId: string) => + openPopup(`/project/modify?objId=${encodeURIComponent(objId)}`, "projectForm", 520, 650); + const openInspection = (objId: string) => + openPopup(`/production/inspection-popup?objId=${encodeURIComponent(objId)}`, "inspection", 1300, 700); + const openFiles = (targetObjId: string, docType: string, docTypeName: string) => + openPopup( + `/common/files?targetObjId=${encodeURIComponent(targetObjId)}&docType=${encodeURIComponent(docType)}&docTypeName=${encodeURIComponent(docTypeName)}`, + "filePopup", 800, 500 + ); + + const handleRegister = () => { + if (selected.length === 0) { + Swal.fire({ icon: "warning", title: "선택된 내용이 없습니다." }); + return; + } + if (selected.length > 1) { + Swal.fire({ icon: "warning", title: "한번에 1개의 내용만 등록 가능합니다." }); + return; + } + openInspection(String(selected[0].OBJID)); + }; + + const columns: GridColumn[] = [ + { + title: "프로젝트정보", + columns: [ + { title: "프로젝트번호", field: "PROJECT_NO", width: 120, hozAlign: "center", frozen: true, + cellClick: (row) => openProjectForm(String(row.OBJID)) }, + { title: "고객사", field: "CUSTOMER_NAME", width: 150, hozAlign: "left" }, + { title: "당사프로젝트명", field: "PROJECT_NAME", width: 250, hozAlign: "left" }, + { title: "요청납기일", field: "REQ_DEL_DATE", width: 100, hozAlign: "center" }, + { title: "셋업지", field: "SETUP", width: 130, hozAlign: "center" }, + { title: "PM", field: "PM_USER_NAME", width: 110, hozAlign: "center" }, + ], + }, + { + title: "검사결과", + columns: [ + { title: "체크리스트", field: "INSPECTION_CNT", width: 110, hozAlign: "center", + formatter: (v) => , + cellClick: (row) => openInspection(String(row.OBJID)) }, + { title: "입회검사", field: "ADMISSION_INSPECTION_CNT", width: 110, hozAlign: "center", + formatter: (v) => , + cellClick: (row) => openFiles(String(row.OBJID), "ADMISSION_INSPECTION_FILE", "입회검사") }, + ], + }, + ]; + + const fetchData = useCallback(async () => { + const res = await fetch("/api/production/inspection", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ year, project_no: projectNo, customer_objid: customerObjid, product, pm_user_id: pmUserId }), + }); + if (res.ok) { + const json = await res.json(); + setData(json.RESULTLIST || []); + } + }, [year, projectNo, customerObjid, product, pmUserId]); + + useEffect(() => { fetchData(); }, [fetchData]); + + useEffect(() => { + (window as unknown as { fn_search?: () => void }).fn_search = fetchData; + return () => { delete (window as unknown as { fn_search?: () => void }).fn_search; }; + }, [fetchData]); + + return ( +
+
+

생산관리_검사관리

+
+ + +
+
+ + + + + + + setProjectNo(e.target.value)} className="w-[180px]" /> + + + + + + + + + + + + + +
+ ); +} diff --git a/src/app/(main)/production/issue/page.tsx b/src/app/(main)/production/issue/page.tsx new file mode 100644 index 0000000..56bde61 --- /dev/null +++ b/src/app/(main)/production/issue/page.tsx @@ -0,0 +1,155 @@ +"use client"; + +import { useState, useCallback, useEffect } from "react"; +import Swal from "sweetalert2"; +import { DataGrid, type GridColumn } from "@/components/grid/data-grid"; +import { SearchForm, SearchField } from "@/components/layout/search-form"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { CodeSelect } from "@/components/ui/code-select"; + +// productionplanning/issuemgmtList.jsp 대응 - 생산관리_이슈관리 +function openPopup(url: string, name: string, w: number, h: number) { + const left = (window.screen.width - w) / 2; + const top = (window.screen.height - h) / 2; + window.open(url, name, `width=${w},height=${h},left=${left},top=${top},scrollbars=yes,resizable=yes`); +} + +export default function ProductionIssuePage() { + const [year, setYear] = useState(new Date().getFullYear().toString()); + const [projectNo, setProjectNo] = useState(""); + const [unitCode, setUnitCode] = useState(""); + const [issueCategory, setIssueCategory] = useState(""); + const [issueType, setIssueType] = useState(""); + const [data, setData] = useState[]>([]); + const [selected, setSelected] = useState[]>([]); + + const openProjectForm = (objId: string) => + openPopup(`/project/modify?objId=${encodeURIComponent(objId)}`, "projectForm", 520, 650); + const openIssueForm = (objId: string) => + openPopup(`/production/issue/form?objId=${encodeURIComponent(objId)}`, "issueForm", 1100, 800); + + const handleRegister = () => openIssueForm(""); + + const fetchData = useCallback(async () => { + const res = await fetch("/api/production/issue", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + year, project_no: projectNo, unit_code: unitCode, + issue_category: issueCategory, issue_type: issueType, + }), + }); + if (res.ok) { + const json = await res.json(); + setData(json.RESULTLIST || []); + } + }, [year, projectNo, unitCode, issueCategory, issueType]); + + useEffect(() => { fetchData(); }, [fetchData]); + + useEffect(() => { + (window as unknown as { fn_search?: () => void }).fn_search = fetchData; + return () => { delete (window as unknown as { fn_search?: () => void }).fn_search; }; + }, [fetchData]); + + const handleAction = async (action: "delete" | "release") => { + if (selected.length === 0) { + Swal.fire({ icon: "warning", title: "선택된 대상이 없습니다." }); + return; + } + const targets = selected.filter((r) => String(r.STATUS || "") === "write"); + if (targets.length === 0) { + Swal.fire({ icon: "warning", title: "등록중인 데이터만 처리 가능합니다." }); + return; + } + const title = action === "delete" ? "선택된 데이터를 삭제하시겠습니까?" : "선택된 데이터를 배포하시겠습니까?"; + const r = await Swal.fire({ + title, icon: "warning", + showCancelButton: true, confirmButtonText: "확인", cancelButtonText: "취소", + }); + if (!r.isConfirmed) return; + + const objIds = targets.map((t) => String(t.OBJID)); + const res = await fetch("/api/production/issue/save", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ actionType: action, objIds }), + }); + const j = await res.json(); + if (j.success) { + await Swal.fire({ icon: "success", title: j.message || "완료", timer: 1000, showConfirmButton: false }); + fetchData(); + } else { + Swal.fire("오류", j.message || "처리 실패", "error"); + } + }; + + const columns: GridColumn[] = [ + { title: "이슈번호", field: "ISSUE_NO", width: 110, hozAlign: "center", + cellClick: (row) => openIssueForm(String(row.OBJID)) }, + { title: "프로젝트번호", field: "PROJECT_NO", width: 110, hozAlign: "center", + cellClick: (row) => openProjectForm(String(row.PROJECT_OBJID)) }, + { title: "유닛명", field: "UNIT_CODE_NAME", width: 140, hozAlign: "left" }, + { title: "품번", field: "PART_NO", width: 120, hozAlign: "left" }, + { title: "품명", field: "PART_NAME", width: 140, hozAlign: "left" }, + { title: "이슈구분", field: "ISSUE_CATEGORY_NAME", width: 100, hozAlign: "center" }, + { title: "이슈유형", field: "ISSUE_TYPE_NAME", width: 100, hozAlign: "center" }, + { title: "이슈내용", field: "CONTENT", width: 220, hozAlign: "left" }, + { title: "등록일", field: "REG_DATE_TEXT", width: 100, hozAlign: "center" }, + { title: "등록자", field: "WRITER_NAME", width: 80, hozAlign: "center" }, + { title: "설계담당자", field: "DESIGN_USERID_NAME", width: 100, hozAlign: "center" }, + { title: "구매담당자", field: "PURCHASE_USERID_NAME", width: 100, hozAlign: "center" }, + { title: "품질담당자", field: "QUALITY_USERID_NAME", width: 100, hozAlign: "center" }, + { title: "생산담당자", field: "PRODUCTION_USERID_NAME", width: 100, hozAlign: "center" }, + { title: "조치결과", field: "DESIGN_RESULT_NAME", width: 100, hozAlign: "center" }, + { title: "조치일", field: "DESIGN_DATE", width: 100, hozAlign: "center" }, + { title: "상태", field: "STATUS_NAME", width: 80, hozAlign: "center" }, + ]; + + return ( +
+
+

생산관리_이슈관리

+
+ + + + +
+
+ + + + + + + setProjectNo(e.target.value)} className="w-[160px]" /> + + + setUnitCode(e.target.value)} className="w-[140px]" /> + + + + + + + + + + +
+ ); +} diff --git a/src/app/(main)/production/page.tsx b/src/app/(main)/production/page.tsx new file mode 100644 index 0000000..5bda54f --- /dev/null +++ b/src/app/(main)/production/page.tsx @@ -0,0 +1,68 @@ +"use client"; + +import { useState, useCallback, useEffect } from "react"; +import { DataGrid, type GridColumn } from "@/components/grid/data-grid"; +import { SearchForm, SearchField } from "@/components/layout/search-form"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; + +// ProductionMng/ProdMgmList.jsp 대응 - 생산관리 +export default function ProductionPage() { + const [year, setYear] = useState(new Date().getFullYear().toString()); + const [projectNo, setProjectNo] = useState(""); + const [data, setData] = useState[]>([]); + + const columns: GridColumn[] = [ + { title: "프로젝트번호", field: "PROJECT_NO", width: 120 }, + { title: "제품명", field: "PRODUCT_NAME", width: 180, hozAlign: "left", + cellClick: (row) => window.open(`/sales/contract/form?objId=${row.OBJID}`, "prodDetail", "width=1200,height=800") }, + { title: "유닛명", field: "UNIT_NAME", width: 150, hozAlign: "left" }, + { title: "생산수량", field: "PROD_QTY", width: 90, hozAlign: "right", formatter: "money" }, + { title: "완료수량", field: "COMPLETE_QTY", width: 90, hozAlign: "right", formatter: "money" }, + { title: "진행율", field: "PROGRESS_RATE", width: 80, hozAlign: "center", + formatter: (_cell, row) => `${row.PROGRESS_RATE || 0}%` }, + { title: "시작일", field: "START_DATE", width: 100, hozAlign: "center" }, + { title: "종료일", field: "END_DATE", width: 100, hozAlign: "center" }, + { title: "상태", field: "STATUS_NAME", width: 80, hozAlign: "center" }, + ]; + + const fetchData = useCallback(async () => { + const res = await fetch("/api/production", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ year, project_no: projectNo }), + }); + if (res.ok) { + const json = await res.json(); + setData(json.RESULTLIST || []); + } + }, [year, projectNo]); + + // 페이지 진입 시 자동 로드 + useEffect(() => { fetchData(); }, [fetchData]); + + return ( +
+
+

생산관리

+
+ +
+
+ + + + + + setProjectNo(e.target.value)} className="w-[130px]" /> + + + +
+ ); +} diff --git a/src/app/(main)/production/planning/page.tsx b/src/app/(main)/production/planning/page.tsx new file mode 100644 index 0000000..2947a32 --- /dev/null +++ b/src/app/(main)/production/planning/page.tsx @@ -0,0 +1,142 @@ +"use client"; + +import { useState, useCallback, useEffect } from "react"; +import { DataGrid, type GridColumn } from "@/components/grid/data-grid"; +import { SearchForm, SearchField } from "@/components/layout/search-form"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { CodeSelect } from "@/components/ui/code-select"; +import { FolderCell } from "@/components/ui/folder-cell"; + +// productionplanning/planningList.jsp 대응 - 생산관리_생산계획수립 +function openPopup(url: string, name: string, w: number, h: number) { + const left = (window.screen.width - w) / 2; + const top = (window.screen.height - h) / 2; + window.open(url, name, `width=${w},height=${h},left=${left},top=${top},scrollbars=yes,resizable=yes`); +} + +export default function ProductionPlanningPage() { + const [year, setYear] = useState(new Date().getFullYear().toString()); + const [projectNo, setProjectNo] = useState(""); + const [customerObjid, setCustomerObjid] = useState(""); + const [product, setProduct] = useState(""); + const [pmUserId, setPmUserId] = useState(""); + const [data, setData] = useState[]>([]); + + const openProjectForm = (objId: string) => + openPopup(`/project/modify?objId=${encodeURIComponent(objId)}`, "projectForm", 520, 650); + const openAssemblyWbs = (objId: string) => + openPopup(`/project/wbs-task?objId=${encodeURIComponent(objId)}`, "wbsTask", 900, 800); + const openSetupWbs = (objId: string) => + openPopup(`/project/wbs-setup?objId=${encodeURIComponent(objId)}`, "wbsSetup", 1100, 750); + + const columns: GridColumn[] = [ + { + title: "프로젝트정보", + columns: [ + { title: "프로젝트번호", field: "PROJECT_NO", width: 120, hozAlign: "center", frozen: true, + cellClick: (row) => openProjectForm(String(row.OBJID)) }, + { title: "고객사", field: "CUSTOMER_NAME", width: 150, hozAlign: "left" }, + { title: "당사프로젝트명", field: "PROJECT_NAME", width: 250, hozAlign: "left" }, + { title: "요청납기일", field: "REQ_DEL_DATE", width: 100, hozAlign: "center" }, + { title: "셋업지", field: "SETUP", width: 110, hozAlign: "center" }, + { title: "PM", field: "PM_USER_NAME", width: 110, hozAlign: "center" }, + ], + }, + { + title: "조립(▤)", + columns: [ + { title: "WBS", field: "WBS_CNT", width: 70, hozAlign: "center", + formatter: (v) => , + cellClick: (row) => openAssemblyWbs(String(row.OBJID)) }, + { + title: "계획", + columns: [ + { title: "시작일", field: "PRODUCE_PLAN_START", width: 100, hozAlign: "center" }, + { title: "종료일", field: "PRODUCE_PLAN_END", width: 100, hozAlign: "center" }, + ], + }, + { + title: "실적", + columns: [ + { title: "시작일", field: "PRODUCE_ACT_START", width: 100, hozAlign: "center" }, + { title: "종료일", field: "PRODUCE_ACT_END", width: 100, hozAlign: "center" }, + ], + }, + ], + }, + { + title: "셋업(▤)", + columns: [ + { title: "WBS", field: "SETUP_WBS_CNT", width: 70, hozAlign: "center", + formatter: (v) => , + cellClick: (row) => openSetupWbs(String(row.OBJID)) }, + { + title: "계획", + columns: [ + { title: "시작일", field: "SETUP_PLAN_START", width: 100, hozAlign: "center" }, + { title: "종료일", field: "SETUP_PLAN_END", width: 100, hozAlign: "center" }, + ], + }, + { + title: "실적", + columns: [ + { title: "시작일", field: "SETUP_ACT_START", width: 100, hozAlign: "center" }, + { title: "종료일", field: "SETUP_ACT_END", width: 100, hozAlign: "center" }, + ], + }, + ], + }, + ]; + + const fetchData = useCallback(async () => { + const res = await fetch("/api/production/planning", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ year, project_no: projectNo, customer_objid: customerObjid, product, pm_user_id: pmUserId }), + }); + if (res.ok) { + const json = await res.json(); + setData(json.RESULTLIST || []); + } + }, [year, projectNo, customerObjid, product, pmUserId]); + + useEffect(() => { fetchData(); }, [fetchData]); + + return ( +
+
+

생산관리_생산계획수립

+
+ +
+
+ + + + + + + setProjectNo(e.target.value)} className="w-[180px]" /> + + + + + + + + + + + + + +
+ ); +} diff --git a/src/app/(main)/production/process/page.tsx b/src/app/(main)/production/process/page.tsx new file mode 100644 index 0000000..7647d07 --- /dev/null +++ b/src/app/(main)/production/process/page.tsx @@ -0,0 +1,119 @@ +"use client"; + +import { useState, useCallback, useEffect } from "react"; +import { DataGrid, type GridColumn } from "@/components/grid/data-grid"; +import { SearchForm, SearchField } from "@/components/layout/search-form"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { CodeSelect } from "@/components/ui/code-select"; + +// productionplanning/processperformanceList.jsp 대응 - 생산관리_공정실적관리 +function openPopup(url: string, name: string, w: number, h: number) { + const left = (window.screen.width - w) / 2; + const top = (window.screen.height - h) / 2; + window.open(url, name, `width=${w},height=${h},left=${left},top=${top},scrollbars=yes,resizable=yes`); +} + +export default function ProductionProcessPage() { + const [year, setYear] = useState(new Date().getFullYear().toString()); + const [projectNo, setProjectNo] = useState(""); + const [customerObjid, setCustomerObjid] = useState(""); + const [data, setData] = useState[]>([]); + + const openProjectForm = (objId: string) => + openPopup(`/project/modify?objId=${encodeURIComponent(objId)}`, "projectForm", 520, 650); + const openAssemblyPopup = (row: Record) => + openPopup( + `/production/assembly-popup?objId=${encodeURIComponent(String(row.OBJID || ""))}`, + "assemblyList", 1300, 800 + ); + + const columns: GridColumn[] = [ + { + title: "프로젝트정보", + columns: [ + { title: "프로젝트번호", field: "PROJECT_NO", width: 120, hozAlign: "center", frozen: true, + cellClick: (row) => openProjectForm(String(row.CONTRACT_OBJID)) }, + { title: "고객사", field: "CUSTOMER_NAME", width: 140, hozAlign: "left" }, + { title: "당사프로젝트명", field: "PROJECT_NAME", width: 250, hozAlign: "left" }, + { title: "요청납기일", field: "DUE_DATE", width: 100, hozAlign: "center" }, + { title: "셋업지", field: "SETUP", width: 110, hozAlign: "center" }, + { title: "PM", field: "PM_USER_NAME", width: 100, hozAlign: "center" }, + ], + }, + { + title: "조립(제작)", + columns: [ + { + title: "E-BOM/구매 BOM", + columns: [ + { title: "유닛명", field: "UNIT_NAME", width: 270, hozAlign: "left" }, + ], + }, + { + title: "생산BOM", + columns: [ + { title: "조립총수", field: "BOM_CNT", width: 90, hozAlign: "right", formatter: "money", + cellClick: (row) => openAssemblyPopup(row) }, + { title: "조립품수", field: "ASSING_CNT", width: 90, hozAlign: "right", formatter: "money", + cellClick: (row) => openAssemblyPopup(row) }, + { title: "공정율(%)", field: "AS_RATE", width: 90, hozAlign: "right" }, + ], + }, + { + title: "작업공수(H)", + columns: [ + { title: "투입공수", field: "TOTAL_SUM", width: 90, hozAlign: "right", formatter: "money" }, + { title: "자사", field: "INSOURCING_SUM", width: 80, hozAlign: "right", formatter: "money" }, + { title: "외주", field: "OUTSOURCING_SUM", width: 80, hozAlign: "right", formatter: "money" }, + ], + }, + ], + }, + ]; + + const fetchData = useCallback(async () => { + const res = await fetch("/api/production/process", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ year, project_no: projectNo, customer_objid: customerObjid }), + }); + if (res.ok) { + const json = await res.json(); + setData(json.RESULTLIST || []); + } + }, [year, projectNo, customerObjid]); + + useEffect(() => { fetchData(); }, [fetchData]); + + return ( +
+
+

생산관리_공정실적관리

+
+ +
+
+ + + + + + + setProjectNo(e.target.value)} className="w-[180px]" /> + + + + + + + +
+ ); +} diff --git a/src/app/(main)/production/release/page.tsx b/src/app/(main)/production/release/page.tsx new file mode 100644 index 0000000..bf1f268 --- /dev/null +++ b/src/app/(main)/production/release/page.tsx @@ -0,0 +1,171 @@ +"use client"; + +import { useState, useCallback, useEffect } from "react"; +import Swal from "sweetalert2"; +import { DataGrid, type GridColumn } from "@/components/grid/data-grid"; +import { SearchForm, SearchField } from "@/components/layout/search-form"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { CodeSelect } from "@/components/ui/code-select"; +import { FolderCell } from "@/components/ui/folder-cell"; + +// productionplanning/releaseMgmtList.jsp 대응 - 생산관리_출고관리 +function openPopup(url: string, name: string, w: number, h: number) { + const left = (window.screen.width - w) / 2; + const top = (window.screen.height - h) / 2; + window.open(url, name, `width=${w},height=${h},left=${left},top=${top},scrollbars=yes,resizable=yes`); +} + +export default function ProductionReleasePage() { + const [year, setYear] = useState(new Date().getFullYear().toString()); + const [projectNo, setProjectNo] = useState(""); + const [customerObjid, setCustomerObjid] = useState(""); + const [product, setProduct] = useState(""); + const [pmUserId, setPmUserId] = useState(""); + const [data, setData] = useState[]>([]); + const [selected, setSelected] = useState[]>([]); + + const openProjectForm = (objId: string) => + openPopup(`/project/modify?objId=${encodeURIComponent(objId)}`, "projectForm", 520, 650); + const openReleaseForm = (row: Record) => { + const rObj = row.RELEASE_OBJID ? String(row.RELEASE_OBJID) : ""; + const qs = new URLSearchParams({ + parentObjId: String(row.OBJID || ""), + product: String(row.PRODUCT || ""), + productGroup: String(row.PRODUCT_GROUP || ""), + ...(rObj ? { objId: rObj } : {}), + }).toString(); + openPopup(`/production/release/form?${qs}`, "releaseForm", 900, 500); + }; + const openInspection = (objId: string) => + openPopup(`/production/inspection-popup?objId=${encodeURIComponent(objId)}`, "inspection", 1300, 700); + const openFiles = (targetObjId: string, docType: string, docTypeName: string) => + openPopup( + `/common/files?targetObjId=${encodeURIComponent(targetObjId)}&docType=${encodeURIComponent(docType)}&docTypeName=${encodeURIComponent(docTypeName)}`, + "filePopup", 800, 500 + ); + + const handleRegister = () => { + if (selected.length === 0) { + Swal.fire({ icon: "warning", title: "선택된 내용이 없습니다." }); + return; + } + if (selected.length > 1) { + Swal.fire({ icon: "warning", title: "한번에 1개의 내용만 등록 가능합니다." }); + return; + } + openReleaseForm(selected[0]); + }; + + const columns: GridColumn[] = [ + { + title: "프로젝트정보", + columns: [ + { title: "프로젝트번호", field: "PROJECT_NO", width: 120, hozAlign: "center", frozen: true, + cellClick: (row) => openProjectForm(String(row.OBJID)) }, + { title: "고객사", field: "CUSTOMER_NAME", width: 140, hozAlign: "left" }, + { title: "당사프로젝트명", field: "PROJECT_NAME", width: 230, hozAlign: "left" }, + { title: "요청납기일", field: "REQ_DEL_DATE", width: 100, hozAlign: "center" }, + { title: "셋업지", field: "SETUP", width: 130, hozAlign: "center" }, + { title: "PM", field: "PM_USER_NAME", width: 100, hozAlign: "center" }, + ], + }, + { + title: "출고관리", + columns: [ + { + title: "검사결과", + columns: [ + { title: "체크리스트", field: "INSPECTION_CNT", width: 100, hozAlign: "center", + formatter: (v) => , + cellClick: (row) => openInspection(String(row.OBJID)) }, + { title: "입회검사", field: "ADMISSION_INSPECTION_CNT", width: 100, hozAlign: "center", + formatter: (v) => , + cellClick: (row) => openFiles(String(row.OBJID), "ADMISSION_INSPECTION_FILE", "입회검사") }, + ], + }, + { + title: "출고내역", + columns: [ + { title: "출하지시서", field: "RELEASE_ORDER_CNT", width: 100, hozAlign: "center", + formatter: (v) => , + cellClick: (row) => { + const rObj = row.RELEASE_OBJID ? String(row.RELEASE_OBJID) : ""; + if (!rObj) { + Swal.fire({ icon: "info", title: "출고 등록 후 첨부 가능합니다." }); + return; + } + openFiles(rObj, "RELEASE_ORDER", "출하지시서"); + } }, + { title: "출고일", field: "RELEASE_DATE", width: 100, hozAlign: "center" }, + { title: "담당자", field: "RELEASE_WRITER", width: 100, hozAlign: "center" }, + { title: "상태", field: "RELEASE_STATUS_TITLE", width: 90, hozAlign: "center" }, + ], + }, + ], + }, + ]; + + const fetchData = useCallback(async () => { + const res = await fetch("/api/production/release", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ year, project_no: projectNo, customer_objid: customerObjid, product, pm_user_id: pmUserId }), + }); + if (res.ok) { + const json = await res.json(); + setData(json.RESULTLIST || []); + } + }, [year, projectNo, customerObjid, product, pmUserId]); + + useEffect(() => { fetchData(); }, [fetchData]); + + useEffect(() => { + (window as unknown as { fn_search?: () => void }).fn_search = fetchData; + return () => { delete (window as unknown as { fn_search?: () => void }).fn_search; }; + }, [fetchData]); + + return ( +
+
+

생산관리_출고관리

+
+ + +
+
+ + + + + + + setProjectNo(e.target.value)} className="w-[180px]" /> + + + + + + + + + + + + + +
+ ); +} diff --git a/src/app/(main)/production/setup/page.tsx b/src/app/(main)/production/setup/page.tsx new file mode 100644 index 0000000..3a75080 --- /dev/null +++ b/src/app/(main)/production/setup/page.tsx @@ -0,0 +1,117 @@ +"use client"; + +import { useState, useCallback, useEffect } from "react"; +import { DataGrid, type GridColumn } from "@/components/grid/data-grid"; +import { SearchForm, SearchField } from "@/components/layout/search-form"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { CodeSelect } from "@/components/ui/code-select"; + +// productionplanning/setupmgmtList.jsp 대응 - 생산관리_셋업관리 +function openPopup(url: string, name: string, w: number, h: number) { + const left = (window.screen.width - w) / 2; + const top = (window.screen.height - h) / 2; + window.open(url, name, `width=${w},height=${h},left=${left},top=${top},scrollbars=yes,resizable=yes`); +} + +export default function ProductionSetupPage() { + const [year, setYear] = useState(new Date().getFullYear().toString()); + const [projectNo, setProjectNo] = useState(""); + const [customerObjid, setCustomerObjid] = useState(""); + const [product, setProduct] = useState(""); + const [pmUserId, setPmUserId] = useState(""); + const [data, setData] = useState[]>([]); + + const openProjectForm = (objId: string) => + openPopup(`/project/modify?objId=${encodeURIComponent(objId)}`, "projectForm", 520, 650); + const openSetupWbs = (objId: string) => + openPopup(`/project/wbs-setup?objId=${encodeURIComponent(objId)}`, "wbsSetup", 1200, 800); + + const columns: GridColumn[] = [ + { + title: "프로젝트정보", + columns: [ + { title: "프로젝트번호", field: "PROJECT_NO", width: 120, hozAlign: "center", frozen: true, + cellClick: (row) => openProjectForm(String(row.OBJID)) }, + { title: "고객사", field: "CUSTOMER_NAME", width: 150, hozAlign: "left" }, + { title: "당사프로젝트명", field: "PROJECT_NAME", width: 240, hozAlign: "left" }, + { title: "요청납기일", field: "REQ_DEL_DATE", width: 100, hozAlign: "center" }, + { title: "셋업지", field: "SETUP", width: 110, hozAlign: "center" }, + { title: "PM", field: "PM_USER_NAME", width: 100, hozAlign: "center" }, + ], + }, + { + title: "셋업", + columns: [ + { + title: "셋업WBS", + columns: [ + { title: "TASK총수", field: "TASK_CNT", width: 90, hozAlign: "right", formatter: "money", + cellClick: (row) => openSetupWbs(String(row.OBJID)) }, + { title: "완료TASK", field: "COMPLETE_CNT", width: 90, hozAlign: "right", formatter: "money" }, + { title: "공정율(%)", field: "SETUP_RATE", width: 90, hozAlign: "right" }, + ], + }, + { + title: "투입인원(명)", + columns: [ + { title: "자사", field: "EMPLOYEES_IN", width: 80, hozAlign: "right", formatter: "money" }, + { title: "외주", field: "EMPLOYEES_OUT", width: 80, hozAlign: "right", formatter: "money" }, + { title: "(계)", field: "EMPLOYEES_TOTAL", width: 80, hozAlign: "right", formatter: "money" }, + ], + }, + ], + }, + ]; + + const fetchData = useCallback(async () => { + const res = await fetch("/api/production/setup", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ year, project_no: projectNo, customer_objid: customerObjid, product, pm_user_id: pmUserId }), + }); + if (res.ok) { + const json = await res.json(); + setData(json.RESULTLIST || []); + } + }, [year, projectNo, customerObjid, product, pmUserId]); + + useEffect(() => { fetchData(); }, [fetchData]); + + return ( +
+
+

생산관리_셋업관리

+
+ +
+
+ + + + + + + setProjectNo(e.target.value)} className="w-[180px]" /> + + + + + + + + + + + + + +
+ ); +} diff --git a/src/app/(main)/production/status/page.tsx b/src/app/(main)/production/status/page.tsx new file mode 100644 index 0000000..a7bd3bd --- /dev/null +++ b/src/app/(main)/production/status/page.tsx @@ -0,0 +1,137 @@ +"use client"; + +import { useState, useCallback, useEffect } from "react"; +import { DataGrid, type GridColumn } from "@/components/grid/data-grid"; +import { SearchForm, SearchField } from "@/components/layout/search-form"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { CodeSelect } from "@/components/ui/code-select"; + +// productionplanning/planningdashboard.jsp 대응 - 생산관리_현황 +function openPopup(url: string, name: string, w: number, h: number) { + const left = (window.screen.width - w) / 2; + const top = (window.screen.height - h) / 2; + window.open(url, name, `width=${w},height=${h},left=${left},top=${top},scrollbars=yes,resizable=yes`); +} + +export default function ProductionStatusPage() { + const [year, setYear] = useState(new Date().getFullYear().toString()); + const [projectNo, setProjectNo] = useState(""); + const [customerObjid, setCustomerObjid] = useState(""); + const [product, setProduct] = useState(""); + const [pmUserId, setPmUserId] = useState(""); + const [data, setData] = useState[]>([]); + + const openProjectForm = (objId: string) => + openPopup(`/project/modify?objId=${encodeURIComponent(objId)}`, "projectForm", 520, 650); + const openAssemblyWbs = (objId: string) => + openPopup(`/project/wbs-task?objId=${encodeURIComponent(objId)}`, "wbsTask", 900, 800); + const openSetupWbs = (objId: string) => + openPopup(`/project/wbs-setup?objId=${encodeURIComponent(objId)}`, "wbsSetup", 1100, 750); + const openIssueList = (objId: string, status: string) => + openPopup( + `/production/issue-popup?status=${encodeURIComponent(status)}&project_no=${encodeURIComponent(objId)}`, + "issueList", 1720, 900 + ); + + const columns: GridColumn[] = [ + { + title: "프로젝트정보", + columns: [ + { title: "프로젝트번호", field: "PROJECT_NO", width: 120, hozAlign: "center", frozen: true, + cellClick: (row) => openProjectForm(String(row.OBJID)) }, + { title: "고객사", field: "CUSTOMER_NAME", width: 130, hozAlign: "left" }, + { title: "당사프로젝트명", field: "PROJECT_NAME", width: 260, hozAlign: "left" }, + { title: "요청납기일", field: "REQ_DEL_DATE", width: 100, hozAlign: "center" }, + { title: "셋업지", field: "SETUP", width: 130, hozAlign: "center" }, + { title: "PM", field: "PM_USER_NAME", width: 90, hozAlign: "center" }, + ], + }, + { + title: "진척관리", + columns: [ + { + title: "조립(제작)", + columns: [ + { title: "공정율(%)", field: "ASSEMBLY_RATE", width: 90, hozAlign: "center", + cellClick: (row) => openAssemblyWbs(String(row.OBJID)) }, + { title: "종료일", field: "ASSEMBLY_DATE_END", width: 100, hozAlign: "center" }, + { title: "투입공수(H)", field: "ASSEMBLY_EMPLOYEES_TOTAL", width: 100, hozAlign: "right", formatter: "money" }, + ], + }, + { + title: "셋업", + columns: [ + { title: "공정율(%)", field: "SETUP_RATE", width: 90, hozAlign: "center", + cellClick: (row) => openSetupWbs(String(row.OBJID)) }, + { title: "종료일", field: "SETUP_ACT_END", width: 100, hozAlign: "center" }, + { title: "투입공수(H)", field: "EMPLOYEES_TOTAL", width: 100, hozAlign: "right", formatter: "money" }, + ], + }, + ], + }, + { + title: "이슈관리", + columns: [ + { title: "조치율(%)", field: "ISSUE_RATE", width: 90, hozAlign: "center" }, + { title: "발생", field: "ISSUE_CNT", width: 80, hozAlign: "center", + cellClick: (row) => openIssueList(String(row.OBJID), "all") }, + { title: "조치", field: "COMP_CNT", width: 80, hozAlign: "center", + cellClick: (row) => openIssueList(String(row.OBJID), "complete") }, + { title: "미결", field: "MISS_CNT", width: 80, hozAlign: "center", + cellClick: (row) => openIssueList(String(row.OBJID), "late") }, + ], + }, + ]; + + const fetchData = useCallback(async () => { + const res = await fetch("/api/production/status", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ year, project_no: projectNo, customer_objid: customerObjid, product, pm_user_id: pmUserId }), + }); + if (res.ok) { + const json = await res.json(); + setData(json.RESULTLIST || []); + } + }, [year, projectNo, customerObjid, product, pmUserId]); + + useEffect(() => { fetchData(); }, [fetchData]); + + return ( +
+
+

생산관리_현황

+
+ +
+
+ + + + + + + setProjectNo(e.target.value)} className="w-[180px]" /> + + + + + + + + + + + + + +
+ ); +} diff --git a/src/app/(main)/project/page.tsx b/src/app/(main)/project/page.tsx new file mode 100644 index 0000000..1be13c6 --- /dev/null +++ b/src/app/(main)/project/page.tsx @@ -0,0 +1,75 @@ +"use client"; + +import { useState, useCallback, useEffect } from "react"; +import { DataGrid, type GridColumn } from "@/components/grid/data-grid"; +import { SearchForm, SearchField } from "@/components/layout/search-form"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; + +// project/projectList.jsp 대응 - 프로젝트관리 +export default function ProjectPage() { + const [year, setYear] = useState(new Date().getFullYear().toString()); + const [projectNo, setProjectNo] = useState(""); + const [projectName, setProjectName] = useState(""); + const [data, setData] = useState[]>([]); + + const columns: GridColumn[] = [ + { title: "프로젝트번호", field: "PROJECT_NO", width: 130, + cellClick: (row) => window.open(`/sales/contract/form?objId=${row.OBJID}`, "projectDetail", "width=1200,height=900") }, + { title: "프로젝트명", field: "PROJECT_NAME", width: 250, hozAlign: "left" }, + { title: "고객사", field: "CUSTOMER_NAME", width: 150, hozAlign: "left" }, + { title: "제품명", field: "PRODUCT_NAME", width: 150, hozAlign: "left" }, + { title: "제조공장", field: "MANUFACTURE_PLANT_NAME", width: 120, hozAlign: "center" }, + { title: "상태", field: "STATUS", width: 80, hozAlign: "center" }, + { title: "진행율", field: "TOTAL_RATE", width: 80, hozAlign: "center", + formatter: (_cell, row) => `${row.TOTAL_RATE || 0}%` }, + { title: "PM", field: "PM_NAME", width: 90, hozAlign: "center" }, + { title: "시작일", field: "START_DATE", width: 100, hozAlign: "center" }, + { title: "종료일", field: "END_DATE", width: 100, hozAlign: "center" }, + ]; + + const fetchData = useCallback(async () => { + const res = await fetch("/api/project", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ year, project_no: projectNo, project_name: projectName }), + }); + if (res.ok) { + const json = await res.json(); + setData(json.RESULTLIST || []); + } + }, [year, projectNo, projectName]); + + // 페이지 진입 시 자동 로드 + useEffect(() => { fetchData(); }, [fetchData]); + + return ( +
+
+

프로젝트관리

+
+ +
+
+ + + + + + + setProjectNo(e.target.value)} className="w-[130px]" /> + + + setProjectName(e.target.value)} className="w-[200px]" /> + + + + +
+ ); +} diff --git a/src/app/(main)/project/progress/page.tsx b/src/app/(main)/project/progress/page.tsx new file mode 100644 index 0000000..c595a0a --- /dev/null +++ b/src/app/(main)/project/progress/page.tsx @@ -0,0 +1,238 @@ +"use client"; + +import { useState, useCallback, useEffect } from "react"; +import { DataGrid, type GridColumn } from "@/components/grid/data-grid"; +import { SearchForm, SearchField } from "@/components/layout/search-form"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { SearchableCodeSelect } from "@/components/ui/searchable-code-select"; +import { SearchableSelect } from "@/components/ui/searchable-select"; +import Swal from "sweetalert2"; + +// 프로젝트관리_일정관리(WBS) (원본: project/projectMgmtWbsList.jsp) +export default function ProjectProgressPage() { + const currentYear = new Date().getFullYear(); + const [year, setYear] = useState(String(currentYear)); + const [projectNo, setProjectNo] = useState(""); + const [categoryCd, setCategoryCd] = useState(""); + const [customerObjid, setCustomerObjid] = useState(""); + const [product, setProduct] = useState(""); + const [contractStartDate, setContractStartDate] = useState(""); + const [contractEndDate, setContractEndDate] = useState(""); + const [location, setLocation] = useState(""); + const [setup, setSetup] = useState(""); + const [pmUserId, setPmUserId] = useState(""); + const [data, setData] = useState[]>([]); + + const [projectNoOpts, setProjectNoOpts] = useState<{ value: string; label: string }[]>([]); + const [customers, setCustomers] = useState<{ value: string; label: string }[]>([]); + const [pmUsers, setPmUsers] = useState<{ value: string; label: string }[]>([]); + + useEffect(() => { + fetch("/api/sales/contract/projects", { method: "POST", headers: { "Content-Type": "application/json" }, body: "{}" }) + .then((r) => r.json()) + .then((d) => setProjectNoOpts((d.rows || []).map((r: Record) => ({ + value: String(r.CODE ?? r.code ?? ""), + label: String(r.NAME ?? r.name ?? ""), + })))) + .catch(() => {}); + fetch("/api/sales/customer", { method: "POST", headers: { "Content-Type": "application/json" }, body: "{}" }) + .then((r) => r.json()) + .then((d) => setCustomers((d.RESULTLIST || []).map((r: Record) => ({ + value: String(r.OBJID), label: String(r.SUPPLY_NAME || ""), + })))) + .catch(() => {}); + fetch("/api/admin/users", { method: "POST", headers: { "Content-Type": "application/json" }, body: "{}" }) + .then((r) => r.json()) + .then((d) => setPmUsers((d.RESULTLIST || []).map((r: Record) => ({ + value: String(r.USER_ID), label: String(r.USER_NAME || ""), + })))) + .catch(() => {}); + }, []); + + const centerPopup = (url: string, name: string, w: number, h: number) => { + const left = (window.screen.width - w) / 2; + const top = (window.screen.height - h) / 2; + window.open(url, name, `width=${w},height=${h},left=${left},top=${top},menubars=no,scrollbars=yes,resizable=yes`); + }; + + const openProjectModify = (objId: string) => + centerPopup(`/project/modify?objId=${objId}&actionType=edit`, "projectModifyPopUp", 500, 400); + const openWbsPopUp = (objId: string, categoryCd: string) => + centerPopup(`/project/wbs-task?OBJID=${objId}&CATEGORY_CD=${categoryCd}`, "wbsTaskPopUp", 1700, 800); + const openSetupWbsPopUp = (objId: string) => + centerPopup(`/project/wbs-setup?OBJID=${objId}`, "setupWbsPopUp", 1100, 750); + + // 카테고리별 클릭 팝업 — 설계/구매/제작은 wbs, 셋업은 setup_wbs + const wbsClick = (row: Record) => + openWbsPopUp(String(row.OBJID), String(row.CATEGORY_CD ?? "")); + const setupClick = (row: Record) => openSetupWbsPopUp(String(row.OBJID)); + + const columns: GridColumn[] = [ + { + title: "프로젝트번호", field: "PROJECT_NO", width: 100, frozen: true, hozAlign: "left", + cellClick: (row) => openProjectModify(String(row.OBJID)), + }, + { + title: "프로젝트정보", headerHozAlign: "center", + columns: [ + { title: "계약구분", field: "CATEGORY_NAME", width: 80, hozAlign: "center" }, + { title: "차수", field: "OVERHAUL_ORDER", width: 60, hozAlign: "center" }, + { title: "고객사", field: "CUSTOMER_NAME", width: 120, hozAlign: "left" }, + { title: "당사프로젝트명", field: "PROJECT_NAME", width: 160, hozAlign: "left" }, + { title: "요청납기일", field: "REQ_DEL_DATE", width: 100, hozAlign: "center" }, + { title: "셋업지", field: "SETUP", width: 90, hozAlign: "center" }, + { title: "PM", field: "PM_USER_NAME", width: 80, hozAlign: "center" }, + { title: "제작공장", field: "MANUFACTURE_PLANT_NAME", width: 100, hozAlign: "center" }, + ], + }, + { + title: "설계 및 구매", headerHozAlign: "center", + columns: [ + { + title: "설계관리", headerHozAlign: "center", + columns: [ + { + title: "공정율(%)", field: "DESIGN_RATETOTAL", width: 90, hozAlign: "center", + formatter: (_c, row) => `${row.DESIGN_RATETOTAL ?? 0}%`, + cellClick: wbsClick, + }, + { title: "🟢 완료", field: "DESIGN_COMP_CNT", width: 80, hozAlign: "center", cellClick: wbsClick }, + { title: "🟡 지연완료", field: "DESIGN_LATE_COMP_CNT", width: 90, hozAlign: "center", cellClick: wbsClick }, + { title: "🔵 진행중", field: "DESIGN_ING_CNT", width: 80, hozAlign: "center", cellClick: wbsClick }, + { title: "🔴 지연", field: "DESIGN_LATE_CNT", width: 80, hozAlign: "center", cellClick: wbsClick }, + ], + }, + { + title: "구매관리", headerHozAlign: "center", + columns: [ + { + title: "공정율(%)", field: "PURCHASE_RATETOTAL", width: 90, hozAlign: "center", + formatter: (_c, row) => `${row.PURCHASE_RATETOTAL ?? 0}%`, + cellClick: wbsClick, + }, + { title: "🟢 완료", field: "PURCHASE_COMP_CNT", width: 80, hozAlign: "center", cellClick: wbsClick }, + { title: "🟡 지연완료", field: "PURCHASE_LATE_COMP_CNT", width: 90, hozAlign: "center", cellClick: wbsClick }, + { title: "🔵 진행중", field: "PURCHASE_ING_CNT", width: 80, hozAlign: "center", cellClick: wbsClick }, + { title: "🔴 지연", field: "PURCHASE_LATE_CNT", width: 80, hozAlign: "center", cellClick: wbsClick }, + ], + }, + ], + }, + { + title: "설비조립 및 셋업", headerHozAlign: "center", + columns: [ + { + title: "조립(제작)관리", headerHozAlign: "center", + columns: [ + { + title: "공정율(%)", field: "PRODUCE_RATETOTAL", width: 90, hozAlign: "center", + formatter: (_c, row) => `${row.PRODUCE_RATETOTAL ?? 0}%`, + cellClick: wbsClick, + }, + { title: "🟢 완료", field: "PRODUCE_COMP_CNT", width: 80, hozAlign: "center", cellClick: wbsClick }, + { title: "🟡 지연완료", field: "PRODUCE_LATE_COMP_CNT", width: 90, hozAlign: "center", cellClick: wbsClick }, + { title: "🔵 진행중", field: "PRODUCE_ING_CNT", width: 80, hozAlign: "center", cellClick: wbsClick }, + { title: "🔴 지연", field: "PRODUCE_LATE_CNT", width: 80, hozAlign: "center", cellClick: wbsClick }, + ], + }, + { + title: "셋업관리", headerHozAlign: "center", + columns: [ + { + title: "공정율(%)", field: "SETUP_RATETOTAL", width: 90, hozAlign: "center", + formatter: (_c, row) => `${row.SETUP_RATETOTAL ?? 0}%`, + cellClick: setupClick, + }, + { title: "완료", field: "SETUP_COMP_CNT", width: 80, hozAlign: "center", cellClick: setupClick }, + { title: "지연완료", field: "SETUP_LATE_COMP_CNT", width: 90, hozAlign: "center", cellClick: setupClick }, + { title: "진행중", field: "SETUP_ING_CNT", width: 80, hozAlign: "center", cellClick: setupClick }, + { title: "지연", field: "SETUP_LATE_CNT", width: 80, hozAlign: "center", cellClick: setupClick }, + ], + }, + ], + }, + ]; + + const fetchData = useCallback(async () => { + try { + const res = await fetch("/api/project/progress", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + Year: year, + project_no: projectNo, + category_cd: categoryCd, + customer_objid: customerObjid, + product, + contract_start_date: contractStartDate, + contract_end_date: contractEndDate, + location, + setup, + pm_user_id: pmUserId, + }), + }); + if (res.ok) { + const json = await res.json(); + setData(json.RESULTLIST || []); + } + } catch { + Swal.fire("오류", "데이터 조회 중 오류가 발생했습니다.", "error"); + } + }, [year, projectNo, categoryCd, customerObjid, product, contractStartDate, contractEndDate, location, setup, pmUserId]); + + useEffect(() => { fetchData(); }, [fetchData]); + + return ( +
+
+

프로젝트관리_일정관리(WBS)

+
+ +
+
+ + + + + + + + + + + + + + + + + + +
+ setContractStartDate(e.target.value)} className="w-[140px]" /> + ~ + setContractEndDate(e.target.value)} className="w-[140px]" /> +
+
+ + setLocation(e.target.value)} className="w-[130px]" /> + + + setSetup(e.target.value)} className="w-[130px]" /> + + + + +
+ + +
+ ); +} diff --git a/src/app/(main)/project/status/page.tsx b/src/app/(main)/project/status/page.tsx new file mode 100644 index 0000000..85d6ce9 --- /dev/null +++ b/src/app/(main)/project/status/page.tsx @@ -0,0 +1,252 @@ +"use client"; + +import { useState, useCallback, useEffect } from "react"; +import { DataGrid, type GridColumn } from "@/components/grid/data-grid"; +import { SearchForm, SearchField } from "@/components/layout/search-form"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { SearchableCodeSelect } from "@/components/ui/searchable-code-select"; +import { SearchableSelect } from "@/components/ui/searchable-select"; +import Swal from "sweetalert2"; + +// 프로젝트관리_진행관리 (원본: project/projectMgmtList.jsp) +export default function ProjectStatusPage() { + const currentYear = new Date().getFullYear(); + const [year, setYear] = useState(String(currentYear)); + const [projectNo, setProjectNo] = useState(""); + const [categoryCd, setCategoryCd] = useState(""); + const [customerObjid, setCustomerObjid] = useState(""); + const [product, setProduct] = useState(""); + const [contractStartDate, setContractStartDate] = useState(""); + const [contractEndDate, setContractEndDate] = useState(""); + const [location, setLocation] = useState(""); + const [setup, setSetup] = useState(""); + const [pmUserId, setPmUserId] = useState(""); + const [data, setData] = useState[]>([]); + + const [projectNoOpts, setProjectNoOpts] = useState<{ value: string; label: string }[]>([]); + const [customers, setCustomers] = useState<{ value: string; label: string }[]>([]); + const [pmUsers, setPmUsers] = useState<{ value: string; label: string }[]>([]); + + useEffect(() => { + fetch("/api/sales/contract/projects", { method: "POST", headers: { "Content-Type": "application/json" }, body: "{}" }) + .then((r) => r.json()) + .then((d) => setProjectNoOpts((d.rows || []).map((r: Record) => ({ + value: String(r.CODE ?? r.code ?? ""), + label: String(r.NAME ?? r.name ?? ""), + })))) + .catch(() => {}); + fetch("/api/sales/customer", { method: "POST", headers: { "Content-Type": "application/json" }, body: "{}" }) + .then((r) => r.json()) + .then((d) => setCustomers((d.RESULTLIST || []).map((r: Record) => ({ + value: String(r.OBJID), label: String(r.SUPPLY_NAME || ""), + })))) + .catch(() => {}); + fetch("/api/admin/users", { method: "POST", headers: { "Content-Type": "application/json" }, body: "{}" }) + .then((r) => r.json()) + .then((d) => setPmUsers((d.RESULTLIST || []).map((r: Record) => ({ + value: String(r.USER_ID), label: String(r.USER_NAME || ""), + })))) + .catch(() => {}); + }, []); + + const centerPopup = (url: string, name: string, w: number, h: number) => { + const left = (window.screen.width - w) / 2; + const top = (window.screen.height - h) / 2; + window.open(url, name, `width=${w},height=${h},left=${left},top=${top},menubars=no,scrollbars=yes,resizable=yes`); + }; + + // 원본: openProjectFormPopUp → /project/projectmodifyPopUp.do?OBJID=xxx (420x350) + const openProjectModify = (objId: string) => centerPopup(`/project/modify?objId=${objId}`, "projectModifyPopUp", 500, 400); + // 원본: openIssueFormPopUp(status,projectobjid) → /productionplanning/issuemgmtList.do (1720x900) + const openIssuePopUp = (status: string, objId: string) => + centerPopup(`/production/issue-popup?status=${status}&project_no=${objId}`, "issueListPopUp", 1720, 900); + // 원본: fn_openInvestmentCostPricePopUp + const openInvestCost = (projectObjId: string, productObjId: string, milestoneObjId: string) => + centerPopup( + `/project/invest-cost?projectObjId=${projectObjId}&productObjId=${productObjId}&milestoneObjId=${milestoneObjId}`, + "investCostPopUp", 1200, 800, + ); + // 원본: wbs_popup + const openWbsPopUp = (objId: string, categoryCd: string) => + centerPopup(`/project/wbs-task?OBJID=${objId}&CATEGORY_CD=${categoryCd}&actionType=view`, "wbsTaskPopUp", 1700, 800); + // 원본: setup_wbs_popup + const openSetupWbsPopUp = (objId: string) => + centerPopup(`/project/wbs-setup?OBJID=${objId}`, "setupWbsPopUp", 1100, 750); + + const columns: GridColumn[] = [ + { + title: "프로젝트번호", field: "PROJECT_NO", width: 100, frozen: true, hozAlign: "left", + cellClick: (row) => openProjectModify(String(row.OBJID)), + }, + { + title: "프로젝트정보", headerHozAlign: "center", + columns: [ + { title: "계약구분", field: "CATEGORY_NAME", width: 80, hozAlign: "center" }, + { title: "차수", field: "OVERHAUL_ORDER", width: 60, hozAlign: "center" }, + { title: "고객사", field: "CUSTOMER_NAME", width: 140, hozAlign: "left" }, + { title: "프로젝트명", field: "PROJECT_NAME", width: 180, hozAlign: "left" }, + { title: "고객납기일", field: "REQ_DEL_DATE", width: 100, hozAlign: "center" }, + { title: "셋업지", field: "SETUP", width: 100, hozAlign: "center" }, + { title: "설비방향", field: "FACILITY_NAME", width: 100, hozAlign: "center" }, + { title: "PM", field: "PM_USER_NAME", width: 90, hozAlign: "center" }, + { title: "제작공장", field: "MANUFACTURE_PLANT_NAME", width: 100, hozAlign: "center" }, + { title: "예상납기일", field: "CONTRACT_DEL_DATE", width: 100, hozAlign: "center" }, + ], + }, + { + title: "현황", headerHozAlign: "center", + columns: [ + { + title: "이슈(건수)", headerHozAlign: "center", + columns: [ + { + title: "발생", field: "ISSUE_CNT", width: 60, hozAlign: "center", + cellClick: (row) => openIssuePopUp("all", String(row.OBJID)), + }, + { + title: "조치", field: "COMP_CNT", width: 60, hozAlign: "center", + cellClick: (row) => openIssuePopUp("complete", String(row.OBJID)), + }, + { + title: "미결", field: "MISS_CNT", width: 60, hozAlign: "center", + cellClick: (row) => openIssuePopUp("late", String(row.OBJID)), + }, + { + title: "조치율", field: "ISSUE_RATE", width: 70, hozAlign: "center", + formatter: (_c, row) => `${row.ISSUE_RATE ?? 0}%`, + }, + ], + }, + { + title: "투입원가", headerHozAlign: "center", + columns: [ + { + title: "목표가", field: "TOTAL_COST_GOAL", width: 110, hozAlign: "right", formatter: "money", + cellClick: (row) => openInvestCost( + String(row.OBJID), String(row.PROD_REL_OBJID ?? ""), String(row.MILESTONE_OBJID ?? ""), + ), + }, + { + title: "투입금액", field: "TOTAL_COST_ACTUAL", width: 110, hozAlign: "right", formatter: "money", + cellClick: (row) => openInvestCost( + String(row.OBJID), String(row.PROD_REL_OBJID ?? ""), String(row.MILESTONE_OBJID ?? ""), + ), + }, + { + title: "투입율(%)", field: "TOTAL_INPUT_RATE", width: 80, hozAlign: "center", + formatter: (_c, row) => `${row.TOTAL_INPUT_RATE ?? 0}%`, + cellClick: (row) => openInvestCost( + String(row.OBJID), String(row.PROD_REL_OBJID ?? ""), String(row.MILESTONE_OBJID ?? ""), + ), + }, + ], + }, + { + title: "진척율(%)", headerHozAlign: "center", + columns: [ + { + title: "전체공정", field: "TOTAL_RATE", width: 80, hozAlign: "center", + formatter: (_c, row) => `${row.TOTAL_RATE ?? 0}%`, + cellClick: (row) => openWbsPopUp(String(row.OBJID), String(row.CATEGORY_CD ?? "")), + }, + { + title: "셋업", field: "SETUP_RATE", width: 70, hozAlign: "center", + formatter: (_c, row) => `${row.SETUP_RATE ?? 0}%`, + cellClick: (row) => openSetupWbsPopUp(String(row.OBJID)), + }, + ], + }, + { + title: "출고", headerHozAlign: "center", + columns: [ + { title: "출고여부", field: "RELEASE_STATUS_TITLE", width: 80, hozAlign: "center" }, + { title: "출고일", field: "RELEASE_DATE", width: 100, hozAlign: "center" }, + ], + }, + ], + }, + ]; + + const fetchData = useCallback(async () => { + try { + const res = await fetch("/api/project/status", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + year, + project_no: projectNo, + category_cd: categoryCd, + customer_objid: customerObjid, + product, + contract_start_date: contractStartDate, + contract_end_date: contractEndDate, + location, + setup, + pm_user_id: pmUserId, + }), + }); + if (res.ok) { + const json = await res.json(); + setData(json.RESULTLIST || []); + } + } catch { + Swal.fire("오류", "데이터 조회 중 오류가 발생했습니다.", "error"); + } + }, [year, projectNo, categoryCd, customerObjid, product, contractStartDate, contractEndDate, location, setup, pmUserId]); + + useEffect(() => { fetchData(); }, [fetchData]); + + return ( +
+
+

프로젝트관리_진행관리

+
+ +
+
+ + + + + + + + + + + + + + + + + + +
+ setContractStartDate(e.target.value)} className="w-[140px]" /> + ~ + setContractEndDate(e.target.value)} className="w-[140px]" /> +
+
+ + setLocation(e.target.value)} className="w-[130px]" /> + + + setSetup(e.target.value)} className="w-[130px]" /> + + + + +
+ + +
+ ); +} diff --git a/src/app/(main)/project/total/page.tsx b/src/app/(main)/project/total/page.tsx new file mode 100644 index 0000000..76486a0 --- /dev/null +++ b/src/app/(main)/project/total/page.tsx @@ -0,0 +1,319 @@ +"use client"; + +import { useState, useCallback, useEffect, useMemo } from "react"; +import { SearchForm, SearchField } from "@/components/layout/search-form"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { SearchableCodeSelect } from "@/components/ui/searchable-code-select"; +import { SearchableSelect } from "@/components/ui/searchable-select"; +import Swal from "sweetalert2"; + +// 프로젝트종합 (원본: project/projectMgmtTotalList.jsp) +export default function ProjectTotalPage() { + const currentYear = new Date().getFullYear(); + const [year, setYear] = useState(String(currentYear)); + const [projectNo, setProjectNo] = useState(""); + const [categoryCd, setCategoryCd] = useState(""); + const [customerObjid, setCustomerObjid] = useState(""); + const [product, setProduct] = useState(""); + const [contractStartDate, setContractStartDate] = useState(""); + const [contractEndDate, setContractEndDate] = useState(""); + const [location, setLocation] = useState(""); + const [setup, setSetup] = useState(""); + const [pmUserId, setPmUserId] = useState(""); + const [rows, setRows] = useState[]>([]); + const [selectedObjId, setSelectedObjId] = useState(""); + + const [projectNoOpts, setProjectNoOpts] = useState<{ value: string; label: string }[]>([]); + const [customers, setCustomers] = useState<{ value: string; label: string }[]>([]); + const [pmUsers, setPmUsers] = useState<{ value: string; label: string }[]>([]); + + // Gantt 윈도우: 현재년도 기준 3년(이전/당해/다음) × 12개월 + const ganttYear = parseInt(year, 10) || currentYear; + const ganttStart = new Date(ganttYear - 1, 0, 1); + const ganttEnd = new Date(ganttYear + 1, 11, 31); + const ganttMonths = useMemo(() => { + const list: { y: number; m: number }[] = []; + for (let y = ganttYear - 1; y <= ganttYear + 1; y++) { + for (let m = 0; m < 12; m++) list.push({ y, m }); + } + return list; + }, [ganttYear]); + const ganttTotalDays = (ganttEnd.getTime() - ganttStart.getTime()) / 86400000 + 1; + + useEffect(() => { + fetch("/api/sales/contract/projects", { method: "POST", headers: { "Content-Type": "application/json" }, body: "{}" }) + .then((r) => r.json()) + .then((d) => setProjectNoOpts((d.rows || []).map((r: Record) => ({ + value: String(r.CODE ?? r.code ?? ""), + label: String(r.NAME ?? r.name ?? ""), + })))) + .catch(() => {}); + fetch("/api/sales/customer", { method: "POST", headers: { "Content-Type": "application/json" }, body: "{}" }) + .then((r) => r.json()) + .then((d) => setCustomers((d.RESULTLIST || []).map((r: Record) => ({ + value: String(r.OBJID), label: String(r.SUPPLY_NAME || ""), + })))) + .catch(() => {}); + fetch("/api/admin/users", { method: "POST", headers: { "Content-Type": "application/json" }, body: "{}" }) + .then((r) => r.json()) + .then((d) => setPmUsers((d.RESULTLIST || []).map((r: Record) => ({ + value: String(r.USER_ID), label: String(r.USER_NAME || ""), + })))) + .catch(() => {}); + }, []); + + const centerPopup = (url: string, name: string, w: number, h: number) => { + const left = (window.screen.width - w) / 2; + const top = (window.screen.height - h) / 2; + window.open(url, name, `width=${w},height=${h},left=${left},top=${top},menubars=no,scrollbars=yes,resizable=yes`); + }; + + const openProjectModify = (objId: string) => centerPopup(`/project/modify?objId=${objId}`, "projectModifyPopUp", 500, 400); + const openWbsPopUp = (objId: string, cat: string) => + centerPopup(`/project/wbs-task?OBJID=${objId}&CATEGORY_CD=${cat}`, "wbsTaskPopUp", 1700, 800); + const openGanttPopUp = (objId: string) => + centerPopup(`/project/wbs-gantt?OBJID=${objId}`, "ganttPopUp", 1800, 800); + + const selectedRow = useMemo( + () => rows.find((r) => String(r.OBJID) === selectedObjId), + [rows, selectedObjId] + ); + + const fetchData = useCallback(async () => { + try { + const res = await fetch("/api/project/total", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + Year: year, + project_no: projectNo, + category_cd: categoryCd, + customer_objid: customerObjid, + product, + contract_start_date: contractStartDate, + contract_end_date: contractEndDate, + location, + setup, + pm_user_id: pmUserId, + }), + }); + if (res.ok) { + const json = await res.json(); + const list = (json.RESULTLIST || []) as Record[]; + setRows(list); + setSelectedObjId(list.length > 0 ? String(list[0].OBJID) : ""); + } + } catch { + Swal.fire("오류", "데이터 조회 중 오류가 발생했습니다.", "error"); + } + }, [year, projectNo, categoryCd, customerObjid, product, contractStartDate, contractEndDate, location, setup, pmUserId]); + + useEffect(() => { fetchData(); }, [fetchData]); + + // YYYY-MM-DD 문자열 → 윈도우 내 % (left, width) + const toPct = (start?: unknown, end?: unknown) => { + const s = String(start ?? "").slice(0, 10); + const e = String(end ?? "").slice(0, 10); + if (!s && !e) return null; + const sDate = s ? new Date(s) : (e ? new Date(e) : null); + const eDate = e ? new Date(e) : (s ? new Date(s) : null); + if (!sDate || !eDate || isNaN(sDate.getTime()) || isNaN(eDate.getTime())) return null; + if (eDate < ganttStart || sDate > ganttEnd) return null; + const sClamp = sDate < ganttStart ? ganttStart : sDate; + const eClamp = eDate > ganttEnd ? ganttEnd : eDate; + const left = ((sClamp.getTime() - ganttStart.getTime()) / 86400000) / ganttTotalDays * 100; + const width = Math.max(0.3, ((eClamp.getTime() - sClamp.getTime()) / 86400000 + 1) / ganttTotalDays * 100); + return { left, width }; + }; + + const todayPct = (() => { + const today = new Date(); + if (today < ganttStart || today > ganttEnd) return null; + return ((today.getTime() - ganttStart.getTime()) / 86400000) / ganttTotalDays * 100; + })(); + + // 진척율 포맷 + const fmtRate = (v: unknown) => (v == null || v === "" ? 0 : Number(v)); + + // 상태 계산: 지연 건수 합 > 0 이면 빨강 + const isLate = (r: Record) => + [r.DESIGN_LATE_CNT, r.PURCHASE_LATE_CNT, r.PRODUCE_LATE_CNT, r.SETUP_LATE_CNT] + .some((v) => Number(v ?? 0) > 0); + + return ( +
+

프로젝트종합

+ + + + + + + + + + + + + + + + + + +
+ setContractStartDate(e.target.value)} className="w-[140px]" /> + ~ + setContractEndDate(e.target.value)} className="w-[140px]" /> +
+
+ + setLocation(e.target.value)} className="w-[130px]" /> + + + setSetup(e.target.value)} className="w-[130px]" /> + + + + +
+ +
+
+ 범례: + 설계 + 구매 + 조립 + 출고 + 셋업 +
+
+ + + +
+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + {ganttMonths.map((mm, i) => ( + + ))} + + + + {rows.length === 0 ? ( + + + + ) : ( + rows.map((row) => { + const objId = String(row.OBJID); + const isSelected = objId === selectedObjId; + const design = toPct(row.DESIGN_ACT_START, row.DESIGN_ACT_END); + const purchase = toPct(row.PURCHASE_ACT_START, row.PURCHASE_ACT_END); + const produce = toPct(row.PRODUCE_ACT_START, row.PRODUCE_ACT_END); + const ship = toPct(row.REQ_DEL_DATE, row.REQ_DEL_DATE); + const setupBar = toPct(row.SETUP_ACT_START, row.SETUP_ACT_END); + const late = isLate(row); + + return ( + setSelectedObjId(objId)} + > + + + + + + + + + + + + + + + ); + }) + )} + +
선택프로젝트 정보진척율(%){ganttYear - 1}년{ganttYear}년{ganttYear + 1}년
고객사제품구분프로젝트번호프로젝트명제작공장요청납기일설계구매조립셋업상태 + {mm.m + 1} +
조회된 데이터가 없습니다.
+ setSelectedObjId(objId)} + /> + {String(row.CUSTOMER_NAME ?? "")}{String(row.PRODUCT_NAME ?? "")} { e.stopPropagation(); openProjectModify(objId); }}> + {String(row.PROJECT_NO ?? "")} + {String(row.PROJECT_NAME ?? "")}{String(row.MANUFACTURE_PLANT_NAME ?? "")}{String(row.REQ_DEL_DATE ?? "")}{fmtRate(row.DESIGN_RATETOTAL)}{fmtRate(row.PURCHASE_RATETOTAL)}{fmtRate(row.PRODUCE_RATETOTAL)}{fmtRate(row.SETUP_RATETOTAL)} + + + {todayPct != null && ( +
+ )} + {design && ( +
+ )} + {purchase && ( +
+ )} + {produce && ( +
+ )} + {ship && ( +
+ )} + {setupBar && ( +
+ )} +
+
+
+
+ ); +} diff --git a/src/app/(main)/project/wbs-template/page.tsx b/src/app/(main)/project/wbs-template/page.tsx new file mode 100644 index 0000000..e08e8a5 --- /dev/null +++ b/src/app/(main)/project/wbs-template/page.tsx @@ -0,0 +1,146 @@ +"use client"; + +import { useState, useCallback, useEffect } from "react"; +import Swal from "sweetalert2"; +import { DataGrid, type GridColumn } from "@/components/grid/data-grid"; +import { SearchForm, SearchField } from "@/components/layout/search-form"; +import { Button } from "@/components/ui/button"; +import { SearchableCodeSelect } from "@/components/ui/searchable-code-select"; + +// 프로젝트관리 > 제품구분_UNIT관리 (project/wbsTemplateMngList.do 대응) +export default function WbsTemplatePage() { + const [product, setProduct] = useState(""); + const [data, setData] = useState[]>([]); + const [selectedRows, setSelectedRows] = useState[]>([]); + + const openCenterPopup = (url: string, name: string, w: number, h: number) => { + const left = (window.screen.width - w) / 2; + const top = (window.screen.height - h) / 2; + window.open(url, name, `width=${w},height=${h},left=${left},top=${top},menubars=no,scrollbars=yes,resizable=yes`); + }; + + const columns: GridColumn[] = [ + { title: "제품구분", field: "PRODUCT_NAME", width: 250, hozAlign: "left" }, + { title: "기계형식", field: "TITLE", width: 250, hozAlign: "left" }, + { + title: "고객사_장비목적", + field: "CUSTOMER_PRODUCT", + hozAlign: "left", + cellClick: (row) => { + openCenterPopup( + `/project/wbs-template/master-form?objId=${row.OBJID}`, + "openTemplateMasterPopUp", + 1000, + 200, + ); + }, + }, + { + title: "UNIT", + field: "WBS_TASK_CNT", + width: 250, + hozAlign: "center", + formatter: (cell) => `📁 ${cell ?? 0}`, + cellClick: (row) => { + openCenterPopup( + `/project/wbs-template/task-list?objId=${row.OBJID}`, + "openWBSTaskListPopUp", + 800, + 700, + ); + }, + }, + { title: "등록자", field: "WRITER_TITLE", width: 250, hozAlign: "left" }, + { title: "등록일", field: "REG_DATE_TITLE", width: 250, hozAlign: "center" }, + ]; + + const fetchData = useCallback(async () => { + const res = await fetch("/api/project/wbs-template", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ product }), + }); + if (res.ok) { + const json = await res.json(); + setData(json.RESULTLIST || []); + } + }, [product]); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + const handleRegister = () => { + if (!product) { + Swal.fire("알림", "제품은 필수값입니다. 제품을 선택해 주세요.", "warning"); + return; + } + openCenterPopup( + `/project/wbs-template/excel-import?product=${encodeURIComponent(product)}`, + "openWBSExcelImportPopUp", + 1340, + 700, + ); + }; + + const handleDelete = async () => { + if (selectedRows.length === 0) { + Swal.fire("알림", "선택된 대상이 없습니다.", "warning"); + return; + } + const r = await Swal.fire({ + title: "삭제하시겠습니까?", + icon: "warning", + showCancelButton: true, + confirmButtonColor: "#3085d6", + cancelButtonColor: "#d33", + confirmButtonText: "확인", + cancelButtonText: "취소", + }); + if (!r.isConfirmed) return; + const res = await fetch("/api/project/wbs-template/delete", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ objIds: selectedRows.map((row) => String(row.OBJID)) }), + }); + const json = await res.json(); + if (json.success) { + Swal.fire({ icon: "success", title: json.message, timer: 1200, showConfirmButton: false }); + fetchData(); + } else { + Swal.fire("오류", json.message, "error"); + } + }; + + return ( +
+
+

프로젝트관리_제품구분_UNIT관리

+
+ + + +
+
+ + + + + + + + +
+ ); +} diff --git a/src/app/(main)/purchase-order/page.tsx b/src/app/(main)/purchase-order/page.tsx new file mode 100644 index 0000000..35766f6 --- /dev/null +++ b/src/app/(main)/purchase-order/page.tsx @@ -0,0 +1,94 @@ +"use client"; + +import { useState, useCallback, useEffect } from "react"; +import { DataGrid, type GridColumn } from "@/components/grid/data-grid"; +import { SearchForm, SearchField } from "@/components/layout/search-form"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import Swal from "sweetalert2"; + +// purchaseOrder/purchaseOrderList.jsp 대응 - 발주관리 +export default function PurchaseOrderPage() { + const [year, setYear] = useState(new Date().getFullYear().toString()); + const [poNo, setPoNo] = useState(""); + const [supplierName, setSupplierName] = useState(""); + const [data, setData] = useState[]>([]); + const [selectedRows, setSelectedRows] = useState[]>([]); + + const columns: GridColumn[] = [ + { title: "발주번호", field: "PO_NO", width: 140, hozAlign: "left", + cellClick: (row) => { + const w = 1150, h = 850; + const left = (window.screen.width - w) / 2, top = (window.screen.height - h) / 2; + window.open(`/purchase-order/form?objId=${row.OBJID}`, "poForm", `width=${w},height=${h},left=${left},top=${top}`); + } }, + { title: "프로젝트번호", field: "PROJECT_NO", width: 120, hozAlign: "center" }, + { title: "협력사", field: "SUPPLIER_NAME", width: 150, hozAlign: "left" }, + { title: "발주명", field: "PO_NAME", width: 200, hozAlign: "left" }, + { title: "발주일", field: "PO_DATE", width: 100, hozAlign: "center" }, + { title: "납기일", field: "DELIVERY_DATE", width: 100, hozAlign: "center" }, + { title: "발주금액", field: "PO_AMOUNT", width: 120, hozAlign: "right", formatter: "money" }, + { title: "상태", field: "STATUS_NAME", width: 80, hozAlign: "center" }, + { title: "등록자", field: "WRITER_NAME", width: 90, hozAlign: "center" }, + ]; + + const fetchData = useCallback(async () => { + const res = await fetch("/api/purchase-order", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ year, po_no: poNo, supplier_name: supplierName }), + }); + if (res.ok) { + const json = await res.json(); + setData(json.RESULTLIST || []); + } + }, [year, poNo, supplierName]); + + // 페이지 진입 시 자동 로드 + useEffect(() => { fetchData(); }, [fetchData]); + + return ( +
+
+

발주관리

+
+ + + +
+
+ + + + + + + setPoNo(e.target.value)} className="w-[140px]" /> + + + setSupplierName(e.target.value)} className="w-[150px]" /> + + + + +
+ ); +} diff --git a/src/app/(main)/purchase/bom/page.tsx b/src/app/(main)/purchase/bom/page.tsx new file mode 100644 index 0000000..4c7c73a --- /dev/null +++ b/src/app/(main)/purchase/bom/page.tsx @@ -0,0 +1,195 @@ +"use client"; + +import { useState, useCallback, useEffect } from "react"; +import { DataGrid, type GridColumn } from "@/components/grid/data-grid"; +import { SearchForm, SearchField } from "@/components/layout/search-form"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { SearchableSelect } from "@/components/ui/searchable-select"; +import { FolderCell } from "@/components/ui/folder-cell"; +import Swal from "sweetalert2"; + +type Option = { value: string; label: string }; + +// 원본: /salesMng/salesBomReportList.do +export default function PurchaseBomPage() { + const [year, setYear] = useState(new Date().getFullYear().toString()); + const [customerCd, setCustomerCd] = useState(""); + const [projectNo, setProjectNo] = useState(""); + const [unitName, setUnitName] = useState(""); + const [writer2Id, setWriter2Id] = useState(""); + const [customerOptions, setCustomerOptions] = useState([]); + const [userOptions, setUserOptions] = useState([]); + const [data, setData] = useState[]>([]); + + useEffect(() => { + fetch("/api/admin/supply", { method: "POST", headers: { "Content-Type": "application/json" }, body: "{}" }) + .then((r) => r.json()) + .then((d) => + setCustomerOptions( + (d.RESULTLIST || []).map((r: Record) => ({ + value: String(r.OBJID), + label: String(r.SUPPLY_NAME), + })) + ) + ) + .catch(() => {}); + fetch("/api/admin/users", { method: "POST", headers: { "Content-Type": "application/json" }, body: "{}" }) + .then((r) => r.json()) + .then((d) => + setUserOptions( + (d.RESULTLIST || []).map((r: Record) => ({ + value: String(r.USER_ID), + label: String(r.USER_NAME), + })) + ) + ) + .catch(() => {}); + }, []); + + const openStructurePopup = (objId: string) => { + if (!objId) return; + const w = 1800, h = 800; + const left = (window.screen.width - w) / 2, top = (window.screen.height - h) / 2; + window.open( + `/purchase/bom/structure?objId=${objId}&readonly=1`, + "bomStructure", + `width=${w},height=${h},left=${left},top=${top}` + ); + }; + + const openSalesBomReport = (objId: string, sbrObjId: string) => { + if (!objId) return; + const w = 1900, h = 900; + const left = (window.screen.width - w) / 2, top = (window.screen.height - h) / 2; + window.open( + `/purchase/bom/form?parentObjId=${objId}&objId=${sbrObjId || ""}`, + "salesMngReport", + `width=${w},height=${h},left=${left},top=${top}` + ); + }; + + const columns: GridColumn[] = [ + { title: "프로젝트번호", field: "PROJECT_NO", width: 110, hozAlign: "left" }, + { title: "요청납기", field: "REQ_DEL_DATE", width: 100, hozAlign: "center" }, + { title: "수주회사", field: "CONTRACT_COMPANY_NAME", width: 110, hozAlign: "left" }, + { title: "제작공장", field: "MANUFACTURE_PLANT_NAME", width: 90, hozAlign: "center" }, + { title: "유닛명", field: "UNIT_NAME", width: 220, hozAlign: "left" }, + { + title: "E-BOM", + field: "BOM_CNT", + width: 80, + hozAlign: "center", + formatter: (cell, row) => ( + openStructurePopup(String(row.OBJID || ""))} /> + ), + }, + { title: "배포일", field: "DEPLOY_DATE", width: 100, hozAlign: "center" }, + { title: "설계담당자", field: "WRITER1_NAME", width: 100, hozAlign: "center" }, + { + title: "구매BOM", + field: "SALES_PART_CNT", + width: 90, + hozAlign: "center", + formatter: (cell, row) => ( + openSalesBomReport(String(row.OBJID || ""), String(row.SBR_OBJID || ""))} + /> + ), + }, + { title: "작성일", field: "REGDATE2", width: 100, hozAlign: "center" }, + { title: "구매담당자", field: "WRITER2_NAME", width: 100, hozAlign: "center" }, + ]; + + const fetchData = useCallback(async () => { + const res = await fetch("/api/purchase/bom", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + year, + customer_cd: customerCd, + project_no: projectNo, + unit_name: unitName, + writer2_id: writer2Id, + }), + }); + if (res.ok) { + const json = await res.json(); + setData(json.RESULTLIST || []); + } else { + const j = await res.json().catch(() => ({})); + Swal.fire("오류", j.message || "조회 실패", "error"); + } + }, [year, customerCd, projectNo, unitName, writer2Id]); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return ( +
+
+

구매BOM관리

+
+ +
+
+ + + + + + + + + + setProjectNo(e.target.value)} + placeholder="프로젝트번호" + className="w-[160px]" + /> + + + setUnitName(e.target.value)} + placeholder="유닛명" + className="w-[160px]" + /> + + + + + + + +
+ ); +} diff --git a/src/app/(main)/purchase/design-change/page.tsx b/src/app/(main)/purchase/design-change/page.tsx new file mode 100644 index 0000000..b65bcdb --- /dev/null +++ b/src/app/(main)/purchase/design-change/page.tsx @@ -0,0 +1,306 @@ +"use client"; + +import { useState, useCallback, useEffect } from "react"; +import { DataGrid, type GridColumn } from "@/components/grid/data-grid"; +import { SearchForm, SearchField } from "@/components/layout/search-form"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { SearchableCodeSelect } from "@/components/ui/searchable-code-select"; +import { SearchableSelect } from "@/components/ui/searchable-select"; +import Swal from "sweetalert2"; + +type Option = { value: string; label: string }; + +// 원본: /salesMng/salesPartChgList.do +export default function DesignChangePage() { + const [year, setYear] = useState(new Date().getFullYear().toString()); + const [projectNo, setProjectNo] = useState(""); + const [partNo, setPartNo] = useState(""); + const [partName, setPartName] = useState(""); + const [revision, setRevision] = useState(""); + const [changeType, setChangeType] = useState(""); + const [changeOption, setChangeOption] = useState(""); + const [partType, setPartType] = useState(""); + const [partWriter, setPartWriter] = useState(""); + const [salesWriter, setSalesWriter] = useState(""); + const [actCd, setActCd] = useState(""); + const [actStatus, setActStatus] = useState(""); + const [eoDateStart, setEoDateStart] = useState(""); + const [eoDateEnd, setEoDateEnd] = useState(""); + const [confirmDateStart, setConfirmDateStart] = useState(""); + const [confirmDateEnd, setConfirmDateEnd] = useState(""); + const [userOptions, setUserOptions] = useState([]); + const [data, setData] = useState[]>([]); + const [selectedRows, setSelectedRows] = useState[]>([]); + + useEffect(() => { + fetch("/api/admin/users", { method: "POST", headers: { "Content-Type": "application/json" }, body: "{}" }) + .then((r) => r.json()) + .then((d) => + setUserOptions( + (d.RESULTLIST || []).map((r: Record) => ({ + value: String(r.USER_ID), + label: String(r.USER_NAME), + })) + ) + ) + .catch(() => {}); + }, []); + + const openPartHistoryPopup = (objId: string) => { + if (!objId) return; + const w = 550, h = 250; + const left = (window.screen.width - w) / 2, top = (window.screen.height - h) / 2; + window.open( + `/purchase/design-change/part-history?objId=${objId}`, + "partHistory", + `width=${w},height=${h},left=${left},top=${top}` + ); + }; + + const openOrderPopup = (pomObjId: string) => { + if (!pomObjId) return; + const w = 950, h = 765; + const left = (window.screen.width - w) / 2, top = (window.screen.height - h) / 2; + window.open( + `/order/list/form?objId=${pomObjId}`, + "purchaseOrderForm", + `width=${w},height=${h},left=${left},top=${top}` + ); + }; + + const openActPopup = (row: Record) => { + const w = 600, h = 350; + const left = (window.screen.width - w) / 2, top = (window.screen.height - h) / 2; + const q = new URLSearchParams({ + objId: String(row.SPC_OBJID || ""), + partObjId: String(row.OBJID || ""), + eoNo: String(row.EO_NO || ""), + partNo: String(row.PART_NO || ""), + projectNo: String(row.PROJECT_NO || ""), + }).toString(); + window.open(`/purchase/design-change/form?${q}`, "actRegist", `width=${w},height=${h},left=${left},top=${top}`); + }; + + const columns: GridColumn[] = [ + { title: "EO NO", field: "EO_NO", width: 90, hozAlign: "left" }, + { title: "프로젝트번호", field: "PROJECT_NO", width: 110, hozAlign: "left" }, + { title: "프로젝트명", field: "CUSTOMER_PROJECT_NAME", width: 160, hozAlign: "left" }, + { title: "모품번", field: "PARENT_PART_INFO", width: 130, hozAlign: "left" }, + { + title: "품번", + field: "PART_NO", + width: 120, + hozAlign: "left", + cellClick: (row) => openPartHistoryPopup(String(row.OBJID || "")), + }, + { title: "품명", field: "PART_NAME", width: 150, hozAlign: "left" }, + { title: "수량", field: "QTY", width: 60, hozAlign: "right" }, + { title: "변경수량", field: "QTY_TEMP", width: 80, hozAlign: "right" }, + { title: "EO구분", field: "CHANGE_TYPE_NAME", width: 90, hozAlign: "center" }, + { title: "EO사유", field: "CHANGE_OPTION_NAME", width: 90, hozAlign: "center" }, + { title: "Revision", field: "REVISION", width: 80, hozAlign: "center" }, + { title: "EO Date", field: "EO_DATE", width: 95, hozAlign: "center" }, + { title: "PART구분", field: "PART_TYPE_NAME", width: 90, hozAlign: "center" }, + { title: "설계담당자", field: "WRITER_NAME", width: 100, hozAlign: "center" }, + { title: "실행일", field: "HIS_REG_DATE_TITLE", width: 90, hozAlign: "center" }, + { title: "구매확인", field: "CONFIRM_DATE", width: 95, hozAlign: "center" }, + { title: "구매담당자", field: "WRITER1_NAME", width: 100, hozAlign: "center" }, + { title: "조치내역", field: "ACT_NAME", width: 90, hozAlign: "center" }, + { + title: "발주서NO", + field: "PURCHASE_ORDER_NO", + width: 110, + hozAlign: "left", + cellClick: (row) => openOrderPopup(String(row.PURCHASE_ORDER_MASTER_OBJID || "")), + }, + { title: "조치결과", field: "ACT_STATUS_NAME", width: 80, hozAlign: "center" }, + ]; + + const fetchData = useCallback(async () => { + const res = await fetch("/api/purchase/design-change", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + year, + project_no: projectNo, + part_no: partNo, + part_name: partName, + revision, + change_type: changeType, + change_option: changeOption, + part_type: partType, + part_writer: partWriter, + sales_writer: salesWriter, + act_cd: actCd, + act_status: actStatus, + eo_date_start: eoDateStart, + eo_date_end: eoDateEnd, + confirm_date_start: confirmDateStart, + confirm_date_end: confirmDateEnd, + }), + }); + if (res.ok) { + const json = await res.json(); + setData(json.RESULTLIST || []); + } else { + const j = await res.json().catch(() => ({})); + Swal.fire("오류", j.message || "조회 실패", "error"); + } + }, [ + year, projectNo, partNo, partName, revision, changeType, changeOption, + partType, partWriter, salesWriter, actCd, actStatus, + eoDateStart, eoDateEnd, confirmDateStart, confirmDateEnd, + ]); + + const handleReceive = async () => { + if (selectedRows.length === 0) { + Swal.fire("알림", "접수할 항목을 선택하세요.", "warning"); + return; + } + const confirm = await Swal.fire({ + title: "접수 확인", + text: `선택한 ${selectedRows.length}건을 접수 처리하시겠습니까?`, + icon: "question", + showCancelButton: true, + confirmButtonText: "접수", + cancelButtonText: "취소", + }); + if (!confirm.isConfirmed) return; + const res = await fetch("/api/purchase/design-change/receipt", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ rows: selectedRows }), + }); + const json = await res.json(); + if (json.success) { + Swal.fire({ icon: "success", title: json.message, timer: 1200, showConfirmButton: false }); + fetchData(); + } else { + Swal.fire("오류", json.message || "접수 실패", "error"); + } + }; + + const handleActRegist = () => { + if (selectedRows.length !== 1) { + Swal.fire("알림", "조치내역을 등록할 항목 1건을 선택하세요.", "warning"); + return; + } + openActPopup(selectedRows[0]); + }; + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return ( +
+
+

설계변경리스트

+
+ + + +
+
+ + + + + + + setProjectNo(e.target.value)} className="w-[140px]" /> + + + setPartNo(e.target.value)} className="w-[120px]" /> + + + setPartName(e.target.value)} className="w-[120px]" /> + + + setRevision(e.target.value)} className="w-[80px]" /> + + + + + + + + + + + + + + + + + + + + + + + +
+ setEoDateStart(e.target.value)} + className="h-9 rounded border border-gray-300 bg-white px-2 text-sm w-[130px]" + /> + ~ + setEoDateEnd(e.target.value)} + className="h-9 rounded border border-gray-300 bg-white px-2 text-sm w-[130px]" + /> +
+
+ +
+ setConfirmDateStart(e.target.value)} + className="h-9 rounded border border-gray-300 bg-white px-2 text-sm w-[130px]" + /> + ~ + setConfirmDateEnd(e.target.value)} + className="h-9 rounded border border-gray-300 bg-white px-2 text-sm w-[130px]" + /> +
+
+
+ + +
+ ); +} diff --git a/src/app/(main)/purchase/request/page.tsx b/src/app/(main)/purchase/request/page.tsx new file mode 100644 index 0000000..fee8727 --- /dev/null +++ b/src/app/(main)/purchase/request/page.tsx @@ -0,0 +1,222 @@ +"use client"; + +import { useState, useCallback, useEffect } from "react"; +import { DataGrid, type GridColumn } from "@/components/grid/data-grid"; +import { SearchForm, SearchField } from "@/components/layout/search-form"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { SearchableCodeSelect } from "@/components/ui/searchable-code-select"; +import { SearchableSelect } from "@/components/ui/searchable-select"; +import Swal from "sweetalert2"; + +type Option = { value: string; label: string }; + +// 원본: /salesMng/salesRequestMngRegList.do +export default function PurchaseRequestPage() { + const [year, setYear] = useState(""); + const [projectNo, setProjectNo] = useState(""); + const [requestCd, setRequestCd] = useState(""); + const [receiptWriter, setReceiptWriter] = useState(""); + const [status, setStatus] = useState(""); + const [receiptDateStart, setReceiptDateStart] = useState(""); + const [receiptDateEnd, setReceiptDateEnd] = useState(""); + const [userOptions, setUserOptions] = useState([]); + const [data, setData] = useState[]>([]); + const [selectedRows, setSelectedRows] = useState[]>([]); + + useEffect(() => { + fetch("/api/admin/users", { method: "POST", headers: { "Content-Type": "application/json" }, body: "{}" }) + .then((r) => r.json()) + .then((d) => + setUserOptions( + (d.RESULTLIST || []).map((r: Record) => ({ + value: String(r.USER_ID), + label: String(r.USER_NAME), + })) + ) + ) + .catch(() => {}); + }, []); + + const openRequestForm = (objId?: string) => { + const url = objId ? `/purchase/request/form?objId=${objId}` : "/purchase/request/form"; + const w = 1100, h = 630; + const left = (window.screen.width - w) / 2, top = (window.screen.height - h) / 2; + window.open(url, "requestForm", `width=${w},height=${h},left=${left},top=${top}`); + }; + + const columns: GridColumn[] = [ + { + title: "요청번호", + field: "REQUEST_MNG_NO", + width: 110, + hozAlign: "left", + cellClick: (row) => openRequestForm(String(row.OBJID || "")), + }, + { title: "요청구분", field: "REQUEST_CD_NAME", width: 80, hozAlign: "center" }, + { title: "프로젝트번호", field: "PROJECT_NUMBER", width: 110, hozAlign: "left" }, + { title: "프로젝트명", field: "PROJECT_NAME", width: 150, hozAlign: "left" }, + { title: "유닛명", field: "UNIT_CODE_NAME", width: 170, hozAlign: "left" }, + { title: "구매요청품 수", field: "ITEMS_QTY", width: 110, hozAlign: "right", formatter: "money" }, + { title: "총 수량", field: "TOTAL_QTY", width: 90, hozAlign: "right", formatter: "money" }, + { title: "요청사유", field: "REQUEST_REASONS_NAME", width: 100, hozAlign: "left" }, + { title: "요청인", field: "REQUEST_USER_NAME", width: 110, hozAlign: "center" }, + { title: "입고요청일", field: "DELIVERY_REQUEST_DATE", width: 100, hozAlign: "center" }, + { title: "상태", field: "STATUS_TITLE", width: 90, hozAlign: "center" }, + { title: "접수자", field: "RECEIPT_USER_NAME", width: 100, hozAlign: "center" }, + { title: "접수일", field: "RECEIPT_DATE", width: 90, hozAlign: "center" }, + { title: "발주서NO", field: "PURCHASE_ORDER_NO_ARR", width: 180, hozAlign: "left" }, + ]; + + const fetchData = useCallback(async () => { + const res = await fetch("/api/purchase/request", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + year, + project_no: projectNo, + request_cd: requestCd, + receipt_writer: receiptWriter, + status, + receipt_date_start: receiptDateStart, + receipt_date_end: receiptDateEnd, + }), + }); + if (res.ok) { + const json = await res.json(); + setData(json.RESULTLIST || []); + } else { + const j = await res.json().catch(() => ({})); + Swal.fire("오류", j.message || "조회 실패", "error"); + } + }, [year, projectNo, requestCd, receiptWriter, status, receiptDateStart, receiptDateEnd]); + + const handleReceive = async () => { + if (selectedRows.length === 0) { + Swal.fire("알림", "접수할 항목을 선택하세요.", "warning"); + return; + } + const confirm = await Swal.fire({ + title: "접수 확인", + text: `선택한 ${selectedRows.length}건을 접수 처리하시겠습니까?`, + icon: "question", + showCancelButton: true, + confirmButtonText: "접수", + cancelButtonText: "취소", + }); + if (!confirm.isConfirmed) return; + const res = await fetch("/api/purchase/request/receipt", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ objIds: selectedRows.map((r) => String(r.OBJID)) }), + }); + const json = await res.json(); + if (json.success) { + Swal.fire({ icon: "success", title: json.message, timer: 1200, showConfirmButton: false }); + fetchData(); + } else { + Swal.fire("오류", json.message || "접수 실패", "error"); + } + }; + + const handleOrderWrite = () => { + Swal.fire("알림", "발주서작성 기능은 준비 중입니다.", "info"); + }; + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return ( +
+
+

구매요청서관리

+
+ + + + +
+
+ + + + + + + setProjectNo(e.target.value)} className="w-[140px]" /> + + + + + + + + + + + +
+ setReceiptDateStart(e.target.value)} + className="h-9 rounded border border-gray-300 bg-white px-2 text-sm w-[130px]" + /> + ~ + setReceiptDateEnd(e.target.value)} + className="h-9 rounded border border-gray-300 bg-white px-2 text-sm w-[130px]" + /> +
+
+
+ + +
+ ); +} diff --git a/src/app/(main)/purchase/stock/page.tsx b/src/app/(main)/purchase/stock/page.tsx new file mode 100644 index 0000000..9327f7e --- /dev/null +++ b/src/app/(main)/purchase/stock/page.tsx @@ -0,0 +1,235 @@ +"use client"; + +import { useState, useCallback, useEffect } from "react"; +import { DataGrid, type GridColumn } from "@/components/grid/data-grid"; +import { SearchForm, SearchField } from "@/components/layout/search-form"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { SearchableCodeSelect } from "@/components/ui/searchable-code-select"; +import { SearchableSelect } from "@/components/ui/searchable-select"; +import Swal from "sweetalert2"; + +type Option = { value: string; label: string }; + +// 원본: /salesMng/salesLongDeliveryList.do +export default function StockPage() { + const [ldPartName, setLdPartName] = useState(""); + const [spec, setSpec] = useState(""); + const [location, setLocation] = useState(""); + const [maker, setMaker] = useState(""); + const [materialCode, setMaterialCode] = useState(""); + const [adminSupply, setAdminSupply] = useState(""); + const [supplyOptions, setSupplyOptions] = useState([]); + const [data, setData] = useState[]>([]); + const [selectedRows, setSelectedRows] = useState[]>([]); + + useEffect(() => { + fetch("/api/admin/supply", { method: "POST", headers: { "Content-Type": "application/json" }, body: "{}" }) + .then((r) => r.json()) + .then((d) => + setSupplyOptions( + (d.RESULTLIST || []).map((r: Record) => ({ + value: String(r.OBJID), + label: String(r.SUPPLY_NAME), + })) + ) + ) + .catch(() => {}); + }, []); + + const openStandardPopup = (objId?: string) => { + const w = 600, h = 300; + const left = (window.screen.width - w) / 2, top = (window.screen.height - h) / 2; + window.open( + `/purchase/stock/form?actionType=STANDARD${objId ? `&objId=${objId}` : ""}`, + "stockStandard", + `width=${w},height=${h},left=${left},top=${top}` + ); + }; + + const openInputPopup = (objId: string) => { + if (!objId) return; + const w = 600, h = 590; + const left = (window.screen.width - w) / 2, top = (window.screen.height - h) / 2; + window.open( + `/purchase/stock/form?actionType=INPUT&objId=${objId}`, + "stockInput", + `width=${w},height=${h},left=${left},top=${top}` + ); + }; + + const openPredictPopup = (objId: string) => { + if (!objId) return; + const w = 900, h = 590; + const left = (window.screen.width - w) / 2, top = (window.screen.height - h) / 2; + window.open( + `/purchase/stock/form?actionType=PREDICT&objId=${objId}`, + "stockPredict", + `width=${w},height=${h},left=${left},top=${top}` + ); + }; + + const columns: GridColumn[] = [ + { + title: "품명", + field: "LD_PART_NAME", + width: 180, + hozAlign: "left", + cellClick: (row) => openStandardPopup(String(row.OBJID || "")), + }, + { title: "사양(규격)", field: "SPEC", width: 300, hozAlign: "left" }, + { title: "Location", field: "LOCATION_NAME", width: 100, hozAlign: "center" }, + { title: "내자/외자", field: "FORM_NO", width: 100, hozAlign: "center" }, + { title: "메이커", field: "MAKER", width: 100, hozAlign: "left" }, + { title: "자재코드", field: "MATERIAL_CODE", width: 160, hozAlign: "left" }, + { title: "공급업체", field: "SUPPLY_NAME", width: 140, hozAlign: "left" }, + { title: "재고수량", field: "INPUT_QTY", width: 85, hozAlign: "right", formatter: "money" }, + { + title: "자재투입이력", + field: "INPUT_CNT", + width: 100, + hozAlign: "right", + formatter: "money", + cellClick: (row) => openInputPopup(String(row.OBJID || "")), + }, + { title: "단가", field: "PRICE", width: 90, hozAlign: "right", formatter: "money" }, + { title: "보유금액", field: "PRICE_SUM", width: 120, hozAlign: "right", formatter: "money" }, + { title: "합계", field: "M_TOTAL", width: 80, hozAlign: "right", formatter: "money" }, + { title: "1월", field: "M01", width: 60, hozAlign: "right", formatter: "money" }, + { title: "2월", field: "M02", width: 60, hozAlign: "right", formatter: "money" }, + { title: "3월", field: "M03", width: 60, hozAlign: "right", formatter: "money" }, + { title: "4월", field: "M04", width: 60, hozAlign: "right", formatter: "money" }, + { title: "5월", field: "M05", width: 60, hozAlign: "right", formatter: "money" }, + { title: "6월", field: "M06", width: 60, hozAlign: "right", formatter: "money" }, + { title: "7월", field: "M07", width: 60, hozAlign: "right", formatter: "money" }, + { title: "8월", field: "M08", width: 60, hozAlign: "right", formatter: "money" }, + { title: "9월", field: "M09", width: 60, hozAlign: "right", formatter: "money" }, + { title: "10월", field: "M10", width: 60, hozAlign: "right", formatter: "money" }, + { title: "11월", field: "M11", width: 60, hozAlign: "right", formatter: "money" }, + { title: "12월", field: "M12", width: 60, hozAlign: "right", formatter: "money" }, + ]; + + const fetchData = useCallback(async () => { + const res = await fetch("/api/purchase/stock", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + ld_part_name: ldPartName, + spec, + Location: location, + maker, + material_code: materialCode, + admin_supply: adminSupply, + }), + }); + if (res.ok) { + const json = await res.json(); + setData(json.RESULTLIST || []); + } else { + const j = await res.json().catch(() => ({})); + Swal.fire("오류", j.message || "조회 실패", "error"); + } + }, [ldPartName, spec, location, maker, materialCode, adminSupply]); + + const totalCost = data.reduce((sum, r) => sum + Number(r.PRICE_SUM || 0), 0); + + const handleDelete = async () => { + if (selectedRows.length === 0) { + Swal.fire("알림", "삭제할 항목을 선택하세요.", "warning"); + return; + } + const confirm = await Swal.fire({ + title: "삭제 확인", + text: `선택한 ${selectedRows.length}건을 삭제하시겠습니까?`, + icon: "question", + showCancelButton: true, + confirmButtonText: "삭제", + cancelButtonText: "취소", + }); + if (!confirm.isConfirmed) return; + const res = await fetch("/api/purchase/stock/delete", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ objIds: selectedRows.map((r) => String(r.OBJID)) }), + }); + const json = await res.json(); + if (json.success) { + Swal.fire({ icon: "success", title: json.message, timer: 1200, showConfirmButton: false }); + fetchData(); + } else { + Swal.fire("오류", json.message || "삭제 실패", "error"); + } + }; + + const handleInputClick = () => { + if (selectedRows.length !== 1) { + Swal.fire("알림", "자재투입할 항목 1건을 선택하세요.", "warning"); + return; + } + openInputPopup(String(selectedRows[0].OBJID || "")); + }; + const handlePredictClick = () => { + if (selectedRows.length !== 1) { + Swal.fire("알림", "예측수량을 등록할 항목 1건을 선택하세요.", "warning"); + return; + } + openPredictPopup(String(selectedRows[0].OBJID || "")); + }; + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return ( +
+
+

재고리스트

+
+ + + + + +
+
+ + + + setLdPartName(e.target.value)} className="w-[160px]" /> + + + setSpec(e.target.value)} className="w-[160px]" /> + + + + + + setMaker(e.target.value)} className="w-[140px]" /> + + + setMaterialCode(e.target.value)} className="w-[140px]" /> + + + + + + +
+ 장납기품 비용(원) : {totalCost.toLocaleString()} +
+ + +
+ ); +} diff --git a/src/app/(main)/purchase/supplier/page.tsx b/src/app/(main)/purchase/supplier/page.tsx new file mode 100644 index 0000000..0f4d29c --- /dev/null +++ b/src/app/(main)/purchase/supplier/page.tsx @@ -0,0 +1,137 @@ +"use client"; + +import { useState, useCallback, useEffect } from "react"; +import { DataGrid, type GridColumn } from "@/components/grid/data-grid"; +import { SearchForm, SearchField } from "@/components/layout/search-form"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { SearchableCodeSelect } from "@/components/ui/searchable-code-select"; +import Swal from "sweetalert2"; + +// 구매관리 > 공급업체관리 — 원본: /admin/supplyMngPagingList.do (admin_supply_mng) +export default function SupplierPage() { + const [supplyCode, setSupplyCode] = useState(""); + const [supplyName, setSupplyName] = useState(""); + const [data, setData] = useState[]>([]); + const [selectedRows, setSelectedRows] = useState[]>([]); + + const openSupplierForm = (objId?: string) => { + const url = objId ? `/purchase/supplier/form?objid=${objId}` : "/purchase/supplier/form"; + const w = 1300; + const h = 620; + const left = (window.screen.width - w) / 2; + const top = (window.screen.height - h) / 2; + window.open(url, "supplierForm", `width=${w},height=${h},left=${left},top=${top}`); + }; + + const columns: GridColumn[] = [ + { title: "NO", field: "RNUM", width: 50, hozAlign: "center" }, + { title: "구분", field: "SUPPLY_CODE_NAME", width: 90, hozAlign: "center" }, + { + title: "고객명", + field: "SUPPLY_NAME", + width: 190, + hozAlign: "left", + cellClick: (row) => openSupplierForm(row.OBJID as string), + }, + { title: "지역", field: "AREA_CD_NAME", width: 110, hozAlign: "center" }, + { title: "대표자명", field: "CHARGE_USER_NAME", width: 90, hozAlign: "center" }, + { title: "업종", field: "SUPPLY_STOCKNAME", width: 280, hozAlign: "left" }, + { title: "업태", field: "SUPPLY_BUSNAME", width: 180, hozAlign: "left" }, + { title: "주소", field: "SUPPLY_ADDRESS", width: 250, hozAlign: "left" }, + { title: "핸드폰", field: "SUPPLY_TEL_NO", width: 150, hozAlign: "center" }, + { title: "등록일", field: "REGDATE", width: 110, hozAlign: "center" }, + ]; + + const fetchData = useCallback(async () => { + const res = await fetch("/api/admin/supply", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ supply_code: supplyCode, supply_name: supplyName }), + }); + if (res.ok) { + const json = await res.json(); + setData(json.RESULTLIST || []); + } + }, [supplyCode, supplyName]); + + const handleDelete = async () => { + if (selectedRows.length === 0) { + Swal.fire("알림", "삭제할 항목을 선택하세요.", "warning"); + return; + } + const result = await Swal.fire({ + title: "삭제 확인", + text: `선택한 ${selectedRows.length}건을 삭제하시겠습니까?`, + icon: "question", + showCancelButton: true, + confirmButtonText: "삭제", + cancelButtonText: "취소", + }); + if (!result.isConfirmed) return; + + const res = await fetch("/api/admin/supply/delete", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ objIds: selectedRows.map((r) => String(r.OBJID)) }), + }); + const json = await res.json(); + if (json.success) { + Swal.fire({ icon: "success", title: json.message, timer: 1200, showConfirmButton: false }); + fetchData(); + } else { + Swal.fire("오류", json.message || "삭제 실패", "error"); + } + }; + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return ( +
+
+

공급업체관리

+
+ + + +
+
+ + + + + + + setSupplyName(e.target.value)} + placeholder="고객명" + className="w-[180px]" + /> + + + + +
+ ); +} diff --git a/src/app/(main)/quality/page.tsx b/src/app/(main)/quality/page.tsx new file mode 100644 index 0000000..dfbe686 --- /dev/null +++ b/src/app/(main)/quality/page.tsx @@ -0,0 +1,66 @@ +"use client"; + +import { useState, useCallback, useEffect } from "react"; +import { DataGrid, type GridColumn } from "@/components/grid/data-grid"; +import { SearchForm, SearchField } from "@/components/layout/search-form"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; + +// quality/qualityList.jsp 대응 - 품질관리 +export default function QualityPage() { + const [year, setYear] = useState(new Date().getFullYear().toString()); + const [partNo, setPartNo] = useState(""); + const [data, setData] = useState[]>([]); + + const columns: GridColumn[] = [ + { title: "프로젝트번호", field: "PROJECT_NO", width: 120 }, + { title: "파트번호", field: "PART_NO", width: 130, hozAlign: "left", + cellClick: (row) => window.open(`/production/inspection/form?objId=${row.OBJID}`, "qualityDetail", "width=1000,height=700") }, + { title: "파트명", field: "PART_NAME", width: 180, hozAlign: "left" }, + { title: "검사유형", field: "TEST_TYPE_NAME", width: 100, hozAlign: "center" }, + { title: "검사결과", field: "TEST_RESULT_NAME", width: 100, hozAlign: "center" }, + { title: "검사일", field: "TEST_DATE", width: 100, hozAlign: "center" }, + { title: "검사자", field: "TESTER_NAME", width: 90, hozAlign: "center" }, + { title: "판정", field: "JUDGMENT", width: 80, hozAlign: "center" }, + ]; + + const fetchData = useCallback(async () => { + const res = await fetch("/api/quality", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ year, part_no: partNo }), + }); + if (res.ok) { + const json = await res.json(); + setData(json.RESULTLIST || []); + } + }, [year, partNo]); + + // 페이지 진입 시 자동 로드 + useEffect(() => { fetchData(); }, [fetchData]); + + return ( +
+
+

품질관리

+
+ +
+
+ + + + + + setPartNo(e.target.value)} className="w-[130px]" /> + + + +
+ ); +} diff --git a/src/app/(main)/sales/contract-dashboard/page.tsx b/src/app/(main)/sales/contract-dashboard/page.tsx new file mode 100644 index 0000000..471e904 --- /dev/null +++ b/src/app/(main)/sales/contract-dashboard/page.tsx @@ -0,0 +1,387 @@ +"use client"; + +import { useState, useCallback, useEffect, useMemo } from "react"; +import { SearchForm, SearchField } from "@/components/layout/search-form"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { SearchableCodeSelect } from "@/components/ui/searchable-code-select"; +import { numberWithCommas } from "@/lib/utils"; +import Swal from "sweetalert2"; + +interface Product { CODE: string; NAME: string } +interface MonthRow { + MM: string; + CONTRACT_CNT_YEAR: number; + CONTRACT_COST_YEAR_ORG: number; + CONTRACT_COST_YEAR: number; + RELEASE_CNT_YEAR: number; + [key: string]: number | string; +} +interface YearGoal { + YEAR: string; + PRICE: number; + YEAR_GOAL_OBJID?: string; + CONTRACT_CNT_YEAR: number; + CONTRACT_COST_YEAR: number; + GOAL_RATE: number; +} + +const PIE_COLORS = ["#3366cc", "#dc3912", "#ff9900", "#109618", "#990099", "#0099c6", "#dd4477", "#66aa00"]; + +// 간단한 SVG 파이 차트 +function PieChart({ data, size = 220 }: { data: { label: string; value: number }[]; size?: number }) { + const total = data.reduce((s, d) => s + d.value, 0); + if (total === 0) { + return
데이터 없음
; + } + const cx = size / 2, cy = size / 2, r = size / 2 - 4; + let acc = 0; + const slices = data.map((d, i) => { + const start = (acc / total) * Math.PI * 2 - Math.PI / 2; + acc += d.value; + const end = (acc / total) * Math.PI * 2 - Math.PI / 2; + const large = end - start > Math.PI ? 1 : 0; + const x1 = cx + r * Math.cos(start); + const y1 = cy + r * Math.sin(start); + const x2 = cx + r * Math.cos(end); + const y2 = cy + r * Math.sin(end); + const mid = (start + end) / 2; + const lx = cx + r * 0.65 * Math.cos(mid); + const ly = cy + r * 0.65 * Math.sin(mid); + const pct = ((d.value / total) * 100); + return { + path: `M ${cx} ${cy} L ${x1} ${y1} A ${r} ${r} 0 ${large} 1 ${x2} ${y2} Z`, + color: PIE_COLORS[i % PIE_COLORS.length], + label: d.label, + value: d.value, + pct, + lx, ly, + }; + }); + + return ( +
+ + {slices.map((s, i) => ( + + + {s.pct >= 5 && ( + + {s.pct.toFixed(0)}% + + )} + + ))} + +
+ {slices.map((s, i) => ( +
+ + {s.label} + {s.value} +
+ ))} +
+
+ ); +} + +// 년도별 영업현황 — 막대 2개(목표/수주) + 꺾은선(달성율) +function GoalComboChart({ years }: { years: YearGoal[] }) { + const W = 340, H = 230, PAD_L = 36, PAD_R = 36, PAD_T = 16, PAD_B = 34; + const maxPrice = Math.max(1, ...years.flatMap((y) => [y.PRICE, y.CONTRACT_COST_YEAR])); + const maxRate = Math.max(100, ...years.map((y) => y.GOAL_RATE)); + const chartW = W - PAD_L - PAD_R; + const chartH = H - PAD_T - PAD_B; + const xStep = chartW / years.length; + const barW = Math.min(18, xStep / 3); + + return ( + + {/* Y축 가이드 */} + + + + {years.map((y, i) => { + const cx = PAD_L + xStep * (i + 0.5); + const hGoal = (y.PRICE / maxPrice) * chartH; + const hActual = (y.CONTRACT_COST_YEAR / maxPrice) * chartH; + const rateY = H - PAD_B - (y.GOAL_RATE / maxRate) * chartH; + return ( + + + + {y.YEAR} + + {y.GOAL_RATE.toFixed(0)}% + {i > 0 && (() => { + const prev = years[i - 1]; + const pcx = PAD_L + xStep * (i - 1 + 0.5); + const prateY = H - PAD_B - (prev.GOAL_RATE / maxRate) * chartH; + return ; + })()} + + ); + })} + + {/* 범례 */} + + + 영업목표(억) + + 수주(억) + + + 달성율(%) + + + ); +} + +export default function ContractDashboardPage() { + const currentYear = new Date().getFullYear(); + const [year, setYear] = useState(String(currentYear)); + const [categoryCd, setCategoryCd] = useState(""); + const [customerObjid, setCustomerObjid] = useState(""); + const [product, setProduct] = useState(""); + + const [products, setProducts] = useState([]); + const [total, setTotal] = useState(null); + const [months, setMonths] = useState([]); + const [years, setYears] = useState([]); + const [customerStats, setCustomerStats] = useState<{ OBJID: string; SUPPLY_NAME: string; TOTAL_SUPPLY_UNIT_CNT: number }[]>([]); + const [customers, setCustomers] = useState<{ value: string; label: string }[]>([]); + const [loading, setLoading] = useState(false); + + useEffect(() => { + fetch("/api/sales/customer", { method: "POST", headers: { "Content-Type": "application/json" }, body: "{}" }) + .then((r) => r.json()).then((d) => setCustomers((d.RESULTLIST || []).map((r: Record) => ({ + value: String(r.OBJID || ""), label: String(r.SUPPLY_NAME || ""), + })))).catch(() => {}); + }, []); + + const fetchAll = useCallback(async () => { + setLoading(true); + try { + const body = { Year: year, category_cd: categoryCd, customer_objid: customerObjid, product }; + const [dashR, goalR, custR] = await Promise.all([ + fetch("/api/sales/contract-dashboard", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) }), + fetch("/api/sales/contract-dashboard/year-goal", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ Year: year }) }), + fetch("/api/sales/contract-dashboard/customer-stats", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ Year: year }) }), + ]); + if (dashR.ok) { + const d = await dashR.json(); + setProducts(d.products || []); + setTotal(d.total || null); + setMonths(d.months || []); + } + if (goalR.ok) { + const g = await goalR.json(); + const ys = (g.years || []) as Record[]; + setYears(ys.map((y) => ({ + YEAR: String(y.YEAR || ""), + PRICE: Number(y.PRICE || 0), + YEAR_GOAL_OBJID: String(y.YEAR_GOAL_OBJID || ""), + CONTRACT_CNT_YEAR: Number(y.CONTRACT_CNT_YEAR || 0), + CONTRACT_COST_YEAR: Number(y.CONTRACT_COST_YEAR || 0), + GOAL_RATE: Number(y.GOAL_RATE || 0), + }))); + } + if (custR.ok) { + const c = await custR.json(); + setCustomerStats(((c.rows || []) as Record[]).map((r) => ({ + OBJID: String(r.OBJID || ""), + SUPPLY_NAME: String(r.SUPPLY_NAME || ""), + TOTAL_SUPPLY_UNIT_CNT: Number(r.TOTAL_SUPPLY_UNIT_CNT || 0), + }))); + } + } finally { + setLoading(false); + } + }, [year, categoryCd, customerObjid, product]); + + useEffect(() => { fetchAll(); }, [fetchAll]); + + // 제품별 연 합계 (파이 데이터) + const productPieData = useMemo(() => { + if (!total) return []; + return products.map((p) => ({ + label: p.NAME, + value: Number(total[`CONTRACT_CNT_MONTH_${p.CODE}`] || 0), + })).filter((d) => d.value > 0); + }, [products, total]); + + const customerPieData = useMemo(() => customerStats.map((c) => ({ + label: c.SUPPLY_NAME, value: c.TOTAL_SUPPLY_UNIT_CNT, + })), [customerStats]); + + const handleRegistGoal = async () => { + const current = years.find((y) => y.YEAR === year); + const { value: priceStr } = await Swal.fire({ + title: `${year}년 영업목표`, + input: "number", + inputLabel: "영업목표 (억원)", + inputValue: String(current?.PRICE || ""), + showCancelButton: true, + confirmButtonText: "저장", + cancelButtonText: "취소", + }); + if (!priceStr) return; + const res = await fetch("/api/sales/contract-dashboard/year-goal/save", { + method: "POST", headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ Year: year, PRICE: priceStr, YEAR_GOAL_OBJID: current?.YEAR_GOAL_OBJID || "" }), + }); + const j = await res.json(); + if (j.success) { + Swal.fire({ icon: "success", title: "저장되었습니다.", timer: 1200, showConfirmButton: false }); + fetchAll(); + } else { + Swal.fire("오류", j.message || "저장 실패", "error"); + } + }; + + return ( +
+
+

영업관리_계약현황

+
+ + +
+
+ + + + + + + + + + + + + + + + + {loading &&
조회 중...
} + +
+ {/* 좌: 계약현황 테이블 */} +
+
■ 계약현황 ({year})
+
+ + + + + + + + + + {products.map((p) => )} + + + + {total && ( + + + {products.map((p) => ( + + ))} + + + + )} + {months.map((m) => ( + + + {products.map((p) => ( + + ))} + + + + ))} + {(!months || months.length === 0) && ( + + )} + +
수주확정 건수매출액
(억원)
출고
{p.NAME}
+ {numberWithCommas(Number(total[`CONTRACT_CNT_MONTH_${p.CODE}`] || 0))} + {(total.CONTRACT_COST_YEAR || 0).toFixed(2)}{numberWithCommas(total.RELEASE_CNT_YEAR || 0)}
{parseInt(m.MM, 10)}월 + {Number(m[`CONTRACT_CNT_MONTH_${p.CODE}`] || 0) > 0 + ? numberWithCommas(Number(m[`CONTRACT_CNT_MONTH_${p.CODE}`] || 0)) + : ""} + {m.CONTRACT_COST_YEAR ? m.CONTRACT_COST_YEAR.toFixed(2) : ""}{m.RELEASE_CNT_YEAR > 0 ? numberWithCommas(m.RELEASE_CNT_YEAR) : ""}
데이터 없음
+
+
+ + {/* 중: 영업목표 테이블 + 제품별현황 파이 */} +
+
+
■ 영업목표
+ + + + + + + + + + + + + + + {years.length === 0 ? ( + + ) : ( + years.slice().reverse().map((y) => ( + + + + + + + + )) + )} + +
년도영업목표
(억원)
현황
계약건수계약금액달성율
데이터 없음
{y.YEAR}{numberWithCommas(y.PRICE)}{numberWithCommas(y.CONTRACT_CNT_YEAR)}{(y.CONTRACT_COST_YEAR || 0).toFixed(2)}{(y.GOAL_RATE || 0).toFixed(1)}%
+
+ +
+
■ 제품별현황
+ +
+
+ + {/* 우: 년도별 영업현황 + 고객사별현황 */} +
+
+
■ 년도별 영업현황
+ +
+ +
+
■ 고객사별현황
+ +
+
+
+
+ ); +} diff --git a/src/app/(main)/sales/contract/page.tsx b/src/app/(main)/sales/contract/page.tsx new file mode 100644 index 0000000..96d6f49 --- /dev/null +++ b/src/app/(main)/sales/contract/page.tsx @@ -0,0 +1,220 @@ +"use client"; + +import { useState, useCallback, useEffect } from "react"; +import { DataGrid, type GridColumn } from "@/components/grid/data-grid"; +import { SearchForm, SearchField } from "@/components/layout/search-form"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { SearchableCodeSelect } from "@/components/ui/searchable-code-select"; +import { FolderCell } from "@/components/ui/folder-cell"; +import Swal from "sweetalert2"; + +// 영업관리 > 계약관리 (원본: contractMgmt/contractList.jsp) +export default function ContractPage() { + const currentYear = new Date().getFullYear(); + const [year, setYear] = useState(String(currentYear)); + const [categoryCd, setCategoryCd] = useState(""); + const [customerObjid, setCustomerObjid] = useState(""); + const [product, setProduct] = useState(""); + const [contractResult, setContractResult] = useState(""); + const [pmUserId, setPmUserId] = useState(""); + const [contractStartDate, setContractStartDate] = useState(""); + const [contractEndDate, setContractEndDate] = useState(""); + const [data, setData] = useState[]>([]); + const [selectedRows, setSelectedRows] = useState[]>([]); + const [customers, setCustomers] = useState<{ value: string; label: string }[]>([]); + const [pmUsers, setPmUsers] = useState<{ value: string; label: string }[]>([]); + + useEffect(() => { + fetch("/api/sales/customer", { + method: "POST", headers: { "Content-Type": "application/json" }, body: "{}", + }).then((r) => r.json()).then((d) => { + setCustomers((d.RESULTLIST || []).map((r: Record) => ({ + value: String(r.OBJID), label: String(r.SUPPLY_NAME || ""), + }))); + }).catch(() => {}); + + fetch("/api/admin/users", { + method: "POST", headers: { "Content-Type": "application/json" }, body: "{}", + }).then((r) => r.json()).then((d) => { + setPmUsers((d.RESULTLIST || []).map((r: Record) => ({ + value: String(r.USER_ID), label: String(r.USER_NAME || ""), + }))); + }).catch(() => {}); + }, []); + + const openContractForm = (objId?: string) => { + const url = objId ? `/sales/contract/form?objId=${objId}` : "/sales/contract/form?actionType=regist"; + const w = 1280, h = 780; + const left = Math.max(0, (window.screen.width - w) / 2); + const top = Math.max(0, (window.screen.availHeight - h) / 2); + window.open(url, "contractForm", `width=${w},height=${h},left=${left},top=${top}`); + }; + + // 그리드 컬럼 — 원본 3개 섹션(영업정보/진행사항/수주정보) 순서대로 플랫하게 배치 + const columns: GridColumn[] = [ + // ----- 영업번호 (frozen) ----- + { + title: "영업번호", field: "CONTRACT_NO", width: 100, frozen: true, + cellClick: (row) => openContractForm(String(row.OBJID || "")), + }, + // ----- 영업정보(상세) ----- + { title: "계약구분", field: "CATEGORY_NAME", width: 80 }, + { title: "차수", field: "OVERHAUL_ORDER", width: 60, hozAlign: "right" }, + { title: "국내/해외", field: "AREA_NAME", width: 80 }, + { title: "고객사", field: "CUSTOMER_NAME", width: 160 }, + { title: "제품구분", field: "PRODUCT_NAME", width: 90 }, + { title: "기계형식", field: "MECHANICAL_TYPE", width: 110 }, + { title: "고객사 프로젝트명", field: "CUSTOMER_PROJECT_NAME", width: 200 }, + { title: "예상납기일", field: "DUE_DATE", width: 100, hozAlign: "center" }, + { title: "입고지", field: "LOCATION", width: 100 }, + { title: "셋업지", field: "SETUP", width: 100 }, + { title: "설비방향", field: "FACILITY_NAME", width: 90 }, + { title: "설비대수", field: "FACILITY_QTY", width: 75, formatter: "money", hozAlign: "right" }, + { title: "설비타입", field: "FACILITY_TYPE", width: 100 }, + { title: "설비길이", field: "FACILITY_DEPTH", width: 90 }, + { title: "담당자", field: "WRITER_NAME", width: 80 }, + { title: "등록일", field: "REG_DATE", width: 100, hozAlign: "center" }, + { + title: "첨부", field: "CU01_CNT", width: 60, hozAlign: "center", + formatter: (_c, row) => ( + 0 + ? () => window.open(`/common/files?objId=${row.OBJID}&docType=contractMgmt01`, "files", "width=800,height=500") + : undefined + } + /> + ), + }, + // ----- 진행사항 ----- + { + title: "검토", field: "CU03_CNT", width: 60, hozAlign: "right", + formatter: (_c, row) => Number(row.CU03_CNT || 0) > 0 ? String(row.CU03_CNT) : "", + }, + { title: "상태", field: "CONTRACT_RESULT_NAME", width: 80 }, + // ----- 수주정보 ----- + { title: "수주일", field: "CONTRACT_DATE", width: 100, hozAlign: "center" }, + { title: "PO계약 No", field: "PO_NO", width: 110 }, + { title: "PM", field: "PM_USER_NAME", width: 80 }, + { title: "통화", field: "CONTRACT_CURRENCY_NAME", width: 70 }, + { title: "수주가", field: "CONTRACT_PRICE_CURRENCY", width: 110, formatter: "money", hozAlign: "right" }, + { title: "당사프로젝트명", field: "PROJECT_NAME", width: 180 }, + { title: "계약납기일", field: "CONTRACT_DEL_DATE", width: 100, hozAlign: "center" }, + { title: "요청납기일", field: "REQ_DEL_DATE", width: 100, hozAlign: "center" }, + { title: "수주회사", field: "CONTRACT_COMPANY_NAME", width: 90 }, + { title: "제작공장", field: "MANUFACTURE_PLANT_NAME", width: 90 }, + ]; + + const fetchData = useCallback(async () => { + const res = await fetch("/api/sales/contract", { + method: "POST", headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + Year: year, + category_cd: categoryCd, + customer_objid: customerObjid, + product, + contract_result: contractResult, + pm_user_id: pmUserId, + contract_start_date: contractStartDate, + contract_end_date: contractEndDate, + }), + }); + if (res.ok) { + const json = await res.json(); + setData(json.RESULTLIST || []); + } + }, [year, categoryCd, customerObjid, product, contractResult, pmUserId, contractStartDate, contractEndDate]); + + useEffect(() => { fetchData(); }, [fetchData]); + + const handleDelete = async () => { + if (selectedRows.length === 0) { + Swal.fire("알림", "선택한 항목이 없습니다.", "warning"); + return; + } + const r = await Swal.fire({ + title: "선택한 계약을 삭제하시겠습니까?", + icon: "warning", showCancelButton: true, + confirmButtonText: "확인", cancelButtonText: "취소", + }); + if (!r.isConfirmed) return; + const res = await fetch("/api/sales/contract/delete", { + method: "POST", headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ objIds: selectedRows.map((row) => String(row.OBJID)) }), + }); + const json = await res.json(); + if (json.success) { + Swal.fire({ icon: "success", title: json.message, timer: 1200, showConfirmButton: false }); + fetchData(); + } else { + Swal.fire("오류", json.message || "삭제 실패", "error"); + } + }; + + return ( +
+
+

영업관리_계약관리

+
+ + + +
+
+ + + + + + + + + + + + + + + +
+ setContractStartDate(e.target.value)} className="w-[140px]" /> + ~ + setContractEndDate(e.target.value)} className="w-[140px]" /> +
+
+ + + + + + +
+ +
총 {data.length}건
+ + +
+ ); +} diff --git a/src/app/(main)/sales/customer/page.tsx b/src/app/(main)/sales/customer/page.tsx new file mode 100644 index 0000000..d9ced65 --- /dev/null +++ b/src/app/(main)/sales/customer/page.tsx @@ -0,0 +1,119 @@ +"use client"; + +import { useState, useCallback, useEffect } from "react"; +import { DataGrid, type GridColumn } from "@/components/grid/data-grid"; +import { SearchForm, SearchField } from "@/components/layout/search-form"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { SearchableCodeSelect } from "@/components/ui/searchable-code-select"; +import Swal from "sweetalert2"; + +// 영업관리 > 고객관리 (원본: contractMgmt/supplyMngList.jsp) +export default function CustomerPage() { + const [supplyName, setSupplyName] = useState(""); + const [supplyCode, setSupplyCode] = useState(""); + const [areaCd, setAreaCd] = useState(""); + const [data, setData] = useState[]>([]); + const [selectedRows, setSelectedRows] = useState[]>([]); + + const openPopup = (objid: string = "") => { + const q = objid ? `?objid=${encodeURIComponent(objid)}` : ""; + window.open(`/sales/customer/form${q}`, "customerForm", "width=900,height=560"); + }; + + const columns: GridColumn[] = [ + { + title: "고객번호", field: "CUS_NO", width: 110, + cellClick: (row) => openPopup(String(row.OBJID || "")), + }, + { title: "고객구분", field: "SUPPLY_CODE_NAME", width: 100 }, + { title: "지역", field: "AREA_CD_NAME", width: 110 }, + { title: "고객사", field: "SUPPLY_NAME", width: 220 }, + { title: "대표자명", field: "CHARGE_USER_NAME", width: 110 }, + { title: "사업자등록번호", field: "BUS_REG_NO", width: 140 }, + { title: "주소", field: "SUPPLY_ADDRESS", width: 280 }, + { title: "연락처", field: "SUPPLY_TEL_NO", width: 130 }, + { title: "E-MAIL", field: "EMAIL", width: 220 }, + { title: "등록일", field: "REGDATE", width: 110, hozAlign: "center" }, + ]; + + const fetchData = useCallback(async () => { + const res = await fetch("/api/sales/customer", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + supply_name: supplyName, + supply_code: supplyCode, + area_cd: areaCd, + }), + }); + if (res.ok) { + const json = await res.json(); + setData(json.RESULTLIST || []); + } + }, [supplyName, supplyCode, areaCd]); + + const handleDelete = async () => { + if (selectedRows.length === 0) { + Swal.fire("알림", "선택한 항목이 없습니다.", "warning"); + return; + } + const r = await Swal.fire({ + title: "선택한 고객정보를 삭제하시겠습니까?", + icon: "warning", + showCancelButton: true, + confirmButtonText: "확인", + cancelButtonText: "취소", + }); + if (!r.isConfirmed) return; + const res = await fetch("/api/sales/customer/delete", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ objIds: selectedRows.map((row) => String(row.OBJID)) }), + }); + const json = await res.json(); + if (json.success) { + Swal.fire({ icon: "success", title: "삭제되었습니다.", timer: 1200, showConfirmButton: false }); + fetchData(); + } else { + Swal.fire("오류", json.message || "삭제 실패", "error"); + } + }; + + useEffect(() => { fetchData(); }, [fetchData]); + + return ( +
+
+

영업관리_고객관리

+
+ + + +
+
+ + + + setSupplyName(e.target.value)} className="w-[200px]" /> + + + + + + + + + +
총 {data.length}건
+ + +
+ ); +} diff --git a/src/app/(main)/sales/page.tsx b/src/app/(main)/sales/page.tsx new file mode 100644 index 0000000..9b9c0e0 --- /dev/null +++ b/src/app/(main)/sales/page.tsx @@ -0,0 +1,74 @@ +"use client"; + +import { useState, useCallback, useEffect } from "react"; +import { DataGrid, type GridColumn } from "@/components/grid/data-grid"; +import { SearchForm, SearchField } from "@/components/layout/search-form"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { CodeSelect } from "@/components/ui/code-select"; + +// salesMng/salesMngBOMList.jsp 대응 - 영업관리 +export default function SalesPage() { + const [year, setYear] = useState(new Date().getFullYear().toString()); + const [customerName, setCustomerName] = useState(""); + const [productCode, setProductCode] = useState(""); + const [data, setData] = useState[]>([]); + + const columns: GridColumn[] = [ + { title: "고객사", field: "CUSTOMER_NAME", width: 150, hozAlign: "left", + cellClick: (row) => window.open(`/sales/contract/form?objId=${row.OBJID}`, "salesDetail", "width=1100,height=800") }, + { title: "제품코드", field: "PRODUCT_CODE", width: 120, hozAlign: "left" }, + { title: "제품명", field: "PRODUCT_NAME", width: 180, hozAlign: "left" }, + { title: "프로젝트번호", field: "PROJECT_NO", width: 120, hozAlign: "center" }, + { title: "수량", field: "QTY", width: 80, hozAlign: "right", formatter: "money" }, + { title: "단가", field: "UNIT_PRICE", width: 100, hozAlign: "right", formatter: "money" }, + { title: "금액", field: "AMOUNT", width: 120, hozAlign: "right", formatter: "money" }, + { title: "상태", field: "STATUS_NAME", width: 80, hozAlign: "center" }, + { title: "등록일", field: "REGDATE", width: 100, hozAlign: "center" }, + ]; + + const fetchData = useCallback(async () => { + const res = await fetch("/api/sales", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ year, customer_name: customerName, product_code: productCode }), + }); + if (res.ok) { + const json = await res.json(); + setData(json.RESULTLIST || []); + } + }, [year, customerName, productCode]); + + // 페이지 진입 시 자동 로드 + useEffect(() => { fetchData(); }, [fetchData]); + + return ( +
+
+

영업관리

+
+ +
+
+ + + + + + + setCustomerName(e.target.value)} className="w-[150px]" /> + + + + + + + +
+ ); +} diff --git a/src/app/(main)/sales/release/page.tsx b/src/app/(main)/sales/release/page.tsx new file mode 100644 index 0000000..429900b --- /dev/null +++ b/src/app/(main)/sales/release/page.tsx @@ -0,0 +1,219 @@ +"use client"; + +import { useState, useCallback, useEffect } from "react"; +import { DataGrid, type GridColumn } from "@/components/grid/data-grid"; +import { SearchForm, SearchField } from "@/components/layout/search-form"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { SearchableCodeSelect } from "@/components/ui/searchable-code-select"; +import { FolderCell } from "@/components/ui/folder-cell"; +import Swal from "sweetalert2"; + +// 영업관리 > 출고관리 (원본: releaseMgmt/releaseMgmtList.jsp) +export default function ReleasePage() { + const currentYear = new Date().getFullYear(); + const [year, setYear] = useState(String(currentYear)); + const [categoryCd, setCategoryCd] = useState(""); + const [customerObjid, setCustomerObjid] = useState(""); + const [product, setProduct] = useState(""); + const [pmUserId, setPmUserId] = useState(""); + const [releaseStartDate, setReleaseStartDate] = useState(""); + const [releaseEndDate, setReleaseEndDate] = useState(""); + const [installResult, setInstallResult] = useState(""); + const [data, setData] = useState[]>([]); + const [selectedRows, setSelectedRows] = useState[]>([]); + const [customers, setCustomers] = useState<{ value: string; label: string }[]>([]); + const [pmUsers, setPmUsers] = useState<{ value: string; label: string }[]>([]); + + useEffect(() => { + fetch("/api/sales/customer", { method: "POST", headers: { "Content-Type": "application/json" }, body: "{}" }) + .then((r) => r.json()).then((d) => setCustomers((d.RESULTLIST || []).map((r: Record) => ({ + value: String(r.OBJID || ""), label: String(r.SUPPLY_NAME || ""), + })))).catch(() => {}); + fetch("/api/admin/users", { method: "POST", headers: { "Content-Type": "application/json" }, body: "{}" }) + .then((r) => r.json()).then((d) => setPmUsers((d.RESULTLIST || []).map((r: Record) => ({ + value: String(r.USER_ID || ""), label: String(r.USER_NAME || ""), + })))).catch(() => {}); + }, []); + + const openForm = (row: Record) => { + const projectObjId = String(row.OBJID || ""); + if (!projectObjId) return; + const w = 900, h = 760; + const left = Math.max(0, (window.screen.width - w) / 2); + const top = Math.max(0, (window.screen.availHeight - h) / 2); + window.open(`/sales/release/form?projectObjId=${projectObjId}`, "releaseForm", `width=${w},height=${h},left=${left},top=${top}`); + }; + + const columns: GridColumn[] = [ + { + title: "프로젝트번호", field: "PROJECT_NO", width: 120, frozen: true, + cellClick: (row) => openForm(row), + }, + // 프로젝트정보 + { title: "계약구분", field: "CATEGORY_NAME", width: 80 }, + { title: "차수", field: "OVERHAUL_ORDER", width: 60, hozAlign: "right" }, + { title: "국내/해외", field: "AREA_NAME", width: 80 }, + { title: "고객사", field: "CUSTOMER_NAME", width: 160 }, + { title: "제품구분", field: "PRODUCT_NAME", width: 90 }, + { title: "기계형식", field: "MECHANICAL_TYPE", width: 100 }, + { title: "당사프로젝트명", field: "PROJECT_NAME", width: 180 }, + { title: "요청납기일", field: "REQ_DEL_DATE", width: 100, hozAlign: "center" }, + { title: "입고지", field: "LOCATION", width: 90 }, + { title: "셋업지", field: "SETUP", width: 90 }, + { title: "설비방향", field: "FACILITY_NAME", width: 80 }, + { title: "설비대수", field: "FACILITY_QTY", width: 70, formatter: "money", hozAlign: "right" }, + { title: "설비타입", field: "FACILITY_TYPE", width: 90 }, + { title: "설비길이", field: "FACILITY_DEPTH", width: 80 }, + { title: "PM", field: "PM_USER_NAME", width: 80 }, + { title: "계약납기일", field: "CONTRACT_DEL_DATE", width: 100, hozAlign: "center" }, + // 출고정보 + { + title: "출고검사", field: "RELEASE_CHECK_CNT", width: 75, hozAlign: "center", + formatter: (_c, row) => ( + 0 + ? () => window.open(`/common/files?objId=${row.OBJID}&docType=RELEASE_CHECK`, "files", "width=800,height=500") + : undefined + } + /> + ), + }, + { + title: "출하지시", field: "RELEASE_ORDER_CNT", width: 75, hozAlign: "center", + formatter: (_c, row) => ( + 0 + ? () => window.open(`/common/files?objId=${row.OBJID}&docType=RELEASE_ORDER`, "files", "width=800,height=500") + : undefined + } + /> + ), + }, + { title: "출고일", field: "RELEASE_DATE", width: 100, hozAlign: "center" }, + { title: "출고결과", field: "RELEASE_STATUS_TITLE", width: 80 }, + // 설치&시운전 + { title: "설치완료일", field: "INSTALL_COMPLETE_DATE", width: 100, hozAlign: "center" }, + { title: "설치결과", field: "INSTALL_RESULT", width: 80 }, + { + title: "인수인계", field: "RELEASE_TAKING_OVER_CNT", width: 80, hozAlign: "center", + formatter: (_c, row) => ( + 0 + ? () => window.open(`/common/files?objId=${row.OBJID}&docType=RELEASE_TAKING_OVER`, "files", "width=800,height=500") + : undefined + } + /> + ), + }, + ]; + + const fetchData = useCallback(async () => { + const res = await fetch("/api/sales/release", { + method: "POST", headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + Year: year, + category_cd: categoryCd, + customer_objid: customerObjid, + product, + pm_user_id: pmUserId, + release_start_date: releaseStartDate, + release_end_date: releaseEndDate, + install_result: installResult, + }), + }); + if (res.ok) { + const json = await res.json(); + setData(json.RESULTLIST || []); + } + }, [year, categoryCd, customerObjid, product, pmUserId, releaseStartDate, releaseEndDate, installResult]); + + useEffect(() => { fetchData(); }, [fetchData]); + + const handleRegister = () => { + if (selectedRows.length === 0) { + Swal.fire("알림", "선택된 내용이 없습니다.", "warning"); + return; + } + if (selectedRows.length > 1) { + Swal.fire("알림", "한번에 1개의 내용만 등록 가능합니다.", "warning"); + return; + } + openForm(selectedRows[0]); + }; + + return ( +
+
+

영업관리_출고관리

+
+ + +
+
+ + + + + + + + + + + + + + + +
+ setReleaseStartDate(e.target.value)} className="w-[140px]" /> + ~ + setReleaseEndDate(e.target.value)} className="w-[140px]" /> +
+
+ + + + + + +
+ +
총 {data.length}건 (수주 완료된 계약만 표시)
+ + +
+ ); +} diff --git a/src/app/(main)/scm/defect/page.tsx b/src/app/(main)/scm/defect/page.tsx new file mode 100644 index 0000000..5b7fd29 --- /dev/null +++ b/src/app/(main)/scm/defect/page.tsx @@ -0,0 +1,80 @@ +"use client"; + +import { useState, useCallback, useEffect } from "react"; +import { DataGrid, type GridColumn } from "@/components/grid/data-grid"; +import { SearchForm, SearchField } from "@/components/layout/search-form"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { CodeSelect } from "@/components/ui/code-select"; + +// scm/scmDefectList.jsp 대응 - 부적합품관리 +export default function ScmDefectPage() { + const [year, setYear] = useState(new Date().getFullYear().toString()); + const [supplierName, setSupplierName] = useState(""); + const [statusCode, setStatusCode] = useState(""); + const [data, setData] = useState[]>([]); + + const columns: GridColumn[] = [ + { title: "부적합번호", field: "DEFECT_NO", width: 140, hozAlign: "left", + cellClick: (row) => window.open(`/scm/defect/form?objId=${row.OBJID}`, "defectDetail", "width=900,height=600") }, + { title: "프로젝트번호", field: "PROJECT_NO", width: 120, hozAlign: "left" }, + { title: "공급업체", field: "SUPPLIER_NAME", width: 150, hozAlign: "left" }, + { title: "파트번호", field: "PART_NO", width: 130, hozAlign: "left" }, + { title: "파트명", field: "PART_NAME", width: 150, hozAlign: "left" }, + { title: "부적합유형", field: "DEFECT_TYPE_NAME", width: 100, hozAlign: "center" }, + { title: "발생일", field: "DEFECT_DATE", width: 100, hozAlign: "center" }, + { title: "수량", field: "DEFECT_QTY", width: 70, hozAlign: "right" }, + { title: "조치내용", field: "ACTION_CONTENT", width: 200, hozAlign: "left" }, + { title: "상태", field: "STATUS_NAME", width: 80, hozAlign: "center" }, + ]; + + const fetchData = useCallback(async () => { + const res = await fetch("/api/scm/defect", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + year, + supplier_name: supplierName, + status_code: statusCode, + }), + }); + if (res.ok) { + const json = await res.json(); + setData(json.RESULTLIST || []); + } + }, [year, supplierName, statusCode]); + + // 페이지 진입 시 자동 로드 + useEffect(() => { fetchData(); }, [fetchData]); + + return ( +
+
+

부적합품관리

+
+ + +
+
+ + + + + + + setSupplierName(e.target.value)} className="w-[150px]" /> + + + + + + + +
+ ); +} diff --git a/src/app/(main)/scm/invoice/page.tsx b/src/app/(main)/scm/invoice/page.tsx new file mode 100644 index 0000000..f7aac10 --- /dev/null +++ b/src/app/(main)/scm/invoice/page.tsx @@ -0,0 +1,82 @@ +"use client"; + +import { useState, useCallback, useEffect } from "react"; +import { DataGrid, type GridColumn } from "@/components/grid/data-grid"; +import { SearchForm, SearchField } from "@/components/layout/search-form"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; + +// scm/scmInvoiceList.jsp 대응 - SCM 거래명세서관리 +export default function ScmInvoicePage() { + const [year, setYear] = useState(new Date().getFullYear().toString()); + const [supplierName, setSupplierName] = useState(""); + const [invoiceDateFrom, setInvoiceDateFrom] = useState(""); + const [invoiceDateTo, setInvoiceDateTo] = useState(""); + const [data, setData] = useState[]>([]); + + const columns: GridColumn[] = [ + { title: "거래명세서번호", field: "INVOICE_NO", width: 140, hozAlign: "left", + cellClick: (row) => window.open(`/scm/invoice/form?objId=${row.OBJID}`, "scmInvoiceDetail", "width=1000,height=700") }, + { title: "공급업체", field: "SUPPLIER_NAME", width: 150, hozAlign: "left" }, + { title: "프로젝트번호", field: "PROJECT_NO", width: 120, hozAlign: "left" }, + { title: "발행일", field: "INVOICE_DATE", width: 100, hozAlign: "center" }, + { title: "공급가액", field: "SUPPLY_AMOUNT", width: 120, hozAlign: "right", formatter: "money" }, + { title: "세액", field: "TAX_AMOUNT", width: 100, hozAlign: "right", formatter: "money" }, + { title: "합계", field: "TOTAL_AMOUNT", width: 120, hozAlign: "right", formatter: "money" }, + { title: "상태", field: "STATUS_NAME", width: 80, hozAlign: "center" }, + { title: "등록자", field: "WRITER_NAME", width: 80, hozAlign: "center" }, + ]; + + const fetchData = useCallback(async () => { + const res = await fetch("/api/scm/invoice", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + year, + supplier_name: supplierName, + invoice_date_from: invoiceDateFrom, + invoice_date_to: invoiceDateTo, + }), + }); + if (res.ok) { + const json = await res.json(); + setData(json.RESULTLIST || []); + } + }, [year, supplierName, invoiceDateFrom, invoiceDateTo]); + + // 페이지 진입 시 자동 로드 + useEffect(() => { fetchData(); }, [fetchData]); + + return ( +
+
+

SCM 거래명세서관리

+
+ +
+
+ + + + + + + setSupplierName(e.target.value)} className="w-[150px]" /> + + + setInvoiceDateFrom(e.target.value)} className="w-[140px]" /> + + + setInvoiceDateTo(e.target.value)} className="w-[140px]" /> + + + + +
+ ); +} diff --git a/src/app/(main)/scm/order/arrival-plan/page.tsx b/src/app/(main)/scm/order/arrival-plan/page.tsx new file mode 100644 index 0000000..bb148e0 --- /dev/null +++ b/src/app/(main)/scm/order/arrival-plan/page.tsx @@ -0,0 +1,99 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; +import { useSearchParams } from "next/navigation"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import Swal from "sweetalert2"; + +interface PartRow { + PART_OBJID: string; PART_NO: string; PART_NAME: string; + ORDER_QTY: string; ARRIVAL_OBJID?: string; + ARRIVAL_QTY: string; ARRIVAL_PLAN_DATE: string; +} + +export default function ArrivalPlanPage() { + const searchParams = useSearchParams(); + const objId = searchParams.get("objId") || ""; + const [parts, setParts] = useState([]); + const [loading, setLoading] = useState(false); + + const fetchData = useCallback(async () => { + if (!objId) return; + const res = await fetch("/api/delivery/acceptance/detail", { + method: "POST", headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ objId }), + }); + const json = await res.json(); + if (json.success) { + setParts((json.PARTS || []).map((p: Record) => ({ + PART_OBJID: String(p.PART_OBJID || ""), + PART_NO: String(p.PART_NO || ""), PART_NAME: String(p.PART_NAME || ""), + ORDER_QTY: String(p.ORDER_QTY || "0"), + ARRIVAL_OBJID: String(p.ARRIVAL_OBJID || ""), + ARRIVAL_QTY: String(p.ARRIVAL_QTY || p.ORDER_QTY || "0"), + ARRIVAL_PLAN_DATE: String(p.ARRIVAL_PLAN_DATE || ""), + }))); + } + }, [objId]); + + useEffect(() => { fetchData(); }, [fetchData]); + + const updatePart = (i: number, k: keyof PartRow, v: string) => + setParts((prev) => { const n = [...prev]; n[i] = { ...n[i], [k]: v }; return n; }); + + const handleSave = async () => { + setLoading(true); + try { + const items = parts.map((p) => ({ + objId: p.ARRIVAL_OBJID || undefined, + parent_objid: objId, order_part_objid: p.PART_OBJID, part_objid: p.PART_OBJID, + arrival_qty: p.ARRIVAL_QTY, arrival_plan_date: p.ARRIVAL_PLAN_DATE, + receipt_qty: "0", + })); + const res = await fetch("/api/delivery/acceptance/save", { + method: "POST", headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ items }), + }); + const json = await res.json(); + if (json.success) { + Swal.fire({ icon: "success", title: "입고계획이 등록되었습니다.", timer: 1500, showConfirmButton: false }); + if (window.opener) { try { window.opener.location.reload(); } catch {} } + } else Swal.fire("오류", json.message, "error"); + } finally { setLoading(false); } + }; + + return ( +
+

입고계획 등록

+
+ + + + + + + + + + + + {parts.map((p, i) => ( + + + + + + + + ))} + +
PART NOPART NAME발주수량계획수량입고계획일
{p.PART_NO}{p.PART_NAME}{p.ORDER_QTY} updatePart(i, "ARRIVAL_QTY", e.target.value)} className="h-7 text-xs text-right" /> updatePart(i, "ARRIVAL_PLAN_DATE", e.target.value)} className="h-7 text-xs" />
+
+
+ + +
+
+ ); +} diff --git a/src/app/(main)/scm/order/page.tsx b/src/app/(main)/scm/order/page.tsx new file mode 100644 index 0000000..239541f --- /dev/null +++ b/src/app/(main)/scm/order/page.tsx @@ -0,0 +1,139 @@ +"use client"; + +import { useState, useCallback, useEffect } from "react"; +import { DataGrid, type GridColumn } from "@/components/grid/data-grid"; +import { SearchForm, SearchField } from "@/components/layout/search-form"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { CodeSelect } from "@/components/ui/code-select"; + +// scm/scmOrderList.jsp 대응 - SCM 발주관리 +export default function ScmOrderPage() { + const [year, setYear] = useState(new Date().getFullYear().toString()); + const [purchaseOrderNo, setPurchaseOrderNo] = useState(""); + const [customerName, setCustomerName] = useState(""); + const [projectNo, setProjectNo] = useState(""); + const [statusCode, setStatusCode] = useState(""); + const [data, setData] = useState[]>([]); + const [selectedRows, setSelectedRows] = useState[]>([]); + + const columns: GridColumn[] = [ + { title: "발주No", field: "PURCHASE_ORDER_NO", width: 140, hozAlign: "left", + cellClick: (row) => window.open(`/scm/order/form?objId=${row.OBJID}`, "scmOrderDetail", "width=1100,height=700") }, + { title: "복합마스터", field: "MULTI_MASTER_YN", width: 90, hozAlign: "center" }, + { title: "영업담당", field: "SALES_MNG_USER_NAME", width: 90, hozAlign: "center" }, + { title: "등록일", field: "REGDATE", width: 100, hozAlign: "center" }, + { title: "고객사", field: "CUSTOMER_NAME", width: 150, hozAlign: "left" }, + { title: "프로젝트번호", field: "PROJECT_NO", width: 120, hozAlign: "left" }, + { title: "유닛명", field: "UNIT_NAME", width: 120, hozAlign: "left" }, + { title: "발주명", field: "TITLE", width: 200, hozAlign: "left" }, + { title: "납기일", field: "DELIVERY_DATE", width: 100, hozAlign: "center" }, + { title: "납품장소", field: "DELIVERY_PLACE_NAME", width: 120, hozAlign: "left" }, + { title: "파트수", field: "PART_CNT", width: 70, hozAlign: "right" }, + { title: "실발주수", field: "REAL_ORDER_CNT", width: 80, hozAlign: "right" }, + { title: "상태", field: "STATUS_NAME", width: 80, hozAlign: "center" }, + { title: "입고수", field: "ARRIVAL_CNT", width: 70, hozAlign: "right" }, + { title: "영업상태", field: "SALES_STATUS", width: 80, hozAlign: "center" }, + { title: "입고수량", field: "RECEIPT_QTY", width: 80, hozAlign: "right" }, + { title: "미납수량", field: "NON_DELIVERY_QTY", width: 80, hozAlign: "right" }, + { title: "불량수량", field: "ERROR_QTY", width: 80, hozAlign: "right" }, + { title: "발행일", field: "ISSUANCE_DATE", width: 100, hozAlign: "center" }, + ]; + + const fetchData = useCallback(async () => { + const res = await fetch("/api/scm/order", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + year, + purchase_order_no: purchaseOrderNo, + customer_name: customerName, + project_no: projectNo, + status_code: statusCode, + }), + }); + if (res.ok) { + const json = await res.json(); + setData(json.RESULTLIST || []); + } + }, [year, purchaseOrderNo, customerName, projectNo, statusCode]); + + const handleReceipt = async () => { + if (selectedRows.length === 0) { + alert("접수할 항목을 선택하세요."); + return; + } + // 선택 건들 일괄 접수 처리 (reception_status='reception') + const objIds = selectedRows.map((r) => String(r.OBJID)); + const res = await fetch("/api/scm/order/receipt", { + method: "POST", headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ objIds }), + }); + const json = await res.json(); + if (json.success) { + alert(json.message || "접수 완료"); + fetchData(); + } else { + alert(json.message || "접수 실패"); + } + }; + + const handleArrivalPlan = () => { + if (selectedRows.length === 0) { + alert("입고계획을 등록할 항목을 선택하세요."); + return; + } + const row = selectedRows[0]; + const w = 900, h = 600; + const left = (window.screen.width - w) / 2, top = (window.screen.height - h) / 2; + window.open(`/scm/order/arrival-plan?objId=${row.OBJID}`, "arrivalPlan", + `width=${w},height=${h},left=${left},top=${top}`); + }; + + // 페이지 진입 시 자동 로드 + useEffect(() => { fetchData(); }, [fetchData]); + + return ( +
+
+

SCM 발주관리

+
+ + + +
+
+ + + + + + + setPurchaseOrderNo(e.target.value)} className="w-[140px]" /> + + + setCustomerName(e.target.value)} className="w-[150px]" /> + + + setProjectNo(e.target.value)} className="w-[140px]" /> + + + + + + + +
+ ); +} diff --git a/src/app/(main)/scm/payment/page.tsx b/src/app/(main)/scm/payment/page.tsx new file mode 100644 index 0000000..fdffbdb --- /dev/null +++ b/src/app/(main)/scm/payment/page.tsx @@ -0,0 +1,89 @@ +"use client"; + +import { useState, useCallback, useEffect } from "react"; +import { DataGrid, type GridColumn } from "@/components/grid/data-grid"; +import { SearchForm, SearchField } from "@/components/layout/search-form"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { CodeSelect } from "@/components/ui/code-select"; + +// scm/scmPaymentList.jsp 대응 - 자금지급관리 +export default function ScmPaymentPage() { + const [year, setYear] = useState(new Date().getFullYear().toString()); + const [supplierName, setSupplierName] = useState(""); + const [paymentDateFrom, setPaymentDateFrom] = useState(""); + const [paymentDateTo, setPaymentDateTo] = useState(""); + const [statusCode, setStatusCode] = useState(""); + const [data, setData] = useState[]>([]); + + const columns: GridColumn[] = [ + { title: "지급번호", field: "PAYMENT_NO", width: 140, hozAlign: "left", + cellClick: (row) => window.open(`/scm/payment/form?objId=${row.OBJID}`, "paymentDetail", "width=1000,height=700") }, + { title: "공급업체", field: "SUPPLIER_NAME", width: 150, hozAlign: "left" }, + { title: "프로젝트번호", field: "PROJECT_NO", width: 120, hozAlign: "left" }, + { title: "지급일", field: "PAYMENT_DATE", width: 100, hozAlign: "center" }, + { title: "지급금액", field: "PAYMENT_AMOUNT", width: 120, hozAlign: "right", formatter: "money" }, + { title: "지급구분", field: "PAYMENT_TYPE_NAME", width: 100, hozAlign: "center" }, + { title: "계좌번호", field: "ACCOUNT_NO", width: 150, hozAlign: "left" }, + { title: "상태", field: "STATUS_NAME", width: 80, hozAlign: "center" }, + { title: "등록자", field: "WRITER_NAME", width: 80, hozAlign: "center" }, + ]; + + const fetchData = useCallback(async () => { + const res = await fetch("/api/scm/payment", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + year, + supplier_name: supplierName, + payment_date_from: paymentDateFrom, + payment_date_to: paymentDateTo, + status_code: statusCode, + }), + }); + if (res.ok) { + const json = await res.json(); + setData(json.RESULTLIST || []); + } + }, [year, supplierName, paymentDateFrom, paymentDateTo, statusCode]); + + // 페이지 진입 시 자동 로드 + useEffect(() => { fetchData(); }, [fetchData]); + + return ( +
+
+

자금지급관리

+
+ + +
+
+ + + + + + + setSupplierName(e.target.value)} className="w-[150px]" /> + + + setPaymentDateFrom(e.target.value)} className="w-[140px]" /> + + + setPaymentDateTo(e.target.value)} className="w-[140px]" /> + + + + + + + +
+ ); +} diff --git a/src/app/(main)/scm/quality/page.tsx b/src/app/(main)/scm/quality/page.tsx new file mode 100644 index 0000000..c17ad1a --- /dev/null +++ b/src/app/(main)/scm/quality/page.tsx @@ -0,0 +1,78 @@ +"use client"; + +import { useState, useCallback, useEffect } from "react"; +import { DataGrid, type GridColumn } from "@/components/grid/data-grid"; +import { SearchForm, SearchField } from "@/components/layout/search-form"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { CodeSelect } from "@/components/ui/code-select"; + +// scm/scmQualityList.jsp 대응 - 공급업체품질관리 +export default function ScmQualityPage() { + const [year, setYear] = useState(new Date().getFullYear().toString()); + const [supplierName, setSupplierName] = useState(""); + const [evaluationPeriod, setEvaluationPeriod] = useState(""); + const [data, setData] = useState[]>([]); + + const columns: GridColumn[] = [ + { title: "공급업체", field: "SUPPLIER_NAME", width: 150, hozAlign: "left" }, + { title: "평가기간", field: "EVALUATION_PERIOD", width: 100, hozAlign: "center" }, + { title: "납기준수율%", field: "DELIVERY_RATE", width: 100, hozAlign: "right" }, + { title: "품질점수", field: "QUALITY_SCORE", width: 80, hozAlign: "right" }, + { title: "불량율%", field: "DEFECT_RATE", width: 80, hozAlign: "right" }, + { title: "납품건수", field: "DELIVERY_CNT", width: 80, hozAlign: "right" }, + { title: "불량건수", field: "DEFECT_CNT", width: 80, hozAlign: "right" }, + { title: "종합등급", field: "TOTAL_GRADE", width: 80, hozAlign: "center" }, + { title: "평가일", field: "EVALUATION_DATE", width: 100, hozAlign: "center" }, + { title: "평가자", field: "EVALUATOR_NAME", width: 80, hozAlign: "center" }, + ]; + + const fetchData = useCallback(async () => { + const res = await fetch("/api/scm/quality", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + year, + supplier_name: supplierName, + evaluation_period: evaluationPeriod, + }), + }); + if (res.ok) { + const json = await res.json(); + setData(json.RESULTLIST || []); + } + }, [year, supplierName, evaluationPeriod]); + + // 페이지 진입 시 자동 로드 + useEffect(() => { fetchData(); }, [fetchData]); + + return ( +
+
+

공급업체품질관리

+
+ +
+
+ + + + + + + setSupplierName(e.target.value)} className="w-[150px]" /> + + + + + + + +
+ ); +} diff --git a/src/app/(main)/work/diary/page.tsx b/src/app/(main)/work/diary/page.tsx new file mode 100644 index 0000000..29b358d --- /dev/null +++ b/src/app/(main)/work/diary/page.tsx @@ -0,0 +1,238 @@ +"use client"; + +import { useCallback, useEffect, useState } from "react"; +import Swal from "sweetalert2"; +import { DataGrid, type GridColumn } from "@/components/grid/data-grid"; +import { SearchForm, SearchField } from "@/components/layout/search-form"; +import { Button } from "@/components/ui/button"; +import { SearchableSelect } from "@/components/ui/searchable-select"; + +// productionplanning/workDiaryList.jsp 대응 - 작업일지 목록 +interface Option { value: string; label: string } + +export default function WorkDiaryPage() { + const [year, setYear] = useState(new Date().getFullYear().toString()); + const [searchDivision, setSearchDivision] = useState(""); + const [projectObjId, setProjectObjId] = useState(""); + const [unitCode, setUnitCode] = useState(""); + const [deptCode, setDeptCode] = useState(""); + const [worker, setWorker] = useState(""); + const [searchStatus, setSearchStatus] = useState(""); + + const [projectOptions, setProjectOptions] = useState([]); + const [unitOptions, setUnitOptions] = useState([]); + const [deptOptions, setDeptOptions] = useState([]); + const [workerOptions, setWorkerOptions] = useState([]); + + const [data, setData] = useState[]>([]); + const [selectedRows, setSelectedRows] = useState[]>([]); + + // 공용 드롭다운 + useEffect(() => { + fetch("/api/common/project-list", { method: "POST", headers: { "Content-Type": "application/json" }, body: "{}" }) + .then((r) => r.json()) + .then((d) => { + const rows = (d.RESULTLIST || []) as Record[]; + setProjectOptions(rows.map((r) => ({ + value: String(r.OBJID ?? ""), + label: String(r.LABEL ?? r.PROJECT_NO ?? ""), + }))); + }) + .catch(() => setProjectOptions([])); + + fetch("/api/common/dept-list", { method: "POST", headers: { "Content-Type": "application/json" }, body: "{}" }) + .then((r) => r.json()) + .then((d) => { + const rows = (d.RESULTLIST || []) as Record[]; + setDeptOptions(rows.map((r) => ({ value: String(r.DEPT_CODE ?? ""), label: String(r.DEPT_NAME ?? "") }))); + }) + .catch(() => setDeptOptions([])); + + fetch("/api/common/user-list", { method: "POST", headers: { "Content-Type": "application/json" }, body: "{}" }) + .then((r) => r.json()) + .then((d) => { + const rows = (d.RESULTLIST || []) as Record[]; + setWorkerOptions(rows.map((r) => ({ + value: String(r.USER_ID ?? ""), + label: `${r.DEPT_NAME ?? ""} / ${r.USER_NAME ?? ""}`, + }))); + }) + .catch(() => setWorkerOptions([])); + }, []); + + const handleProjectChange = useCallback((next: string) => { + setProjectObjId(next); + setUnitCode(""); + if (!next) { setUnitOptions([]); return; } + fetch("/api/common/unit-list", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ contract_objid: next }), + }) + .then((r) => r.json()) + .then((d) => { + const list = (d.RESULTLIST || []) as Record[]; + setUnitOptions(list.map((r) => ({ value: String(r.OBJID ?? ""), label: String(r.UNIT_NAME ?? "") }))); + }) + .catch(() => setUnitOptions([])); + }, []); + + const fetchData = useCallback(async () => { + const res = await fetch("/api/work/diary", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + year, + search_division: searchDivision, + project_nos: projectObjId ? [projectObjId] : [], + unit_code: unitCode, + busUsersDeptId: deptCode, + worker, + search_status: searchStatus, + }), + }); + if (res.ok) { + const json = await res.json(); + setData(json.RESULTLIST || []); + } + }, [year, searchDivision, projectObjId, unitCode, deptCode, worker, searchStatus]); + + useEffect(() => { fetchData(); }, [fetchData]); + + const openFormPopup = (objId?: string) => { + const w = 1200; const h = 600; + const left = (window.screen.width - w) / 2; + const top = (window.screen.height - h) / 2; + const url = objId ? `/work/diary/form?objId=${encodeURIComponent(objId)}` : "/work/diary/form"; + window.open(url, "diaryForm", `width=${w},height=${h},left=${left},top=${top}`); + }; + + const collectWriteIds = () => selectedRows + .filter((r) => r.STATUS === "write") + .map((r) => String(r.OBJID)); + + const handleDelete = async () => { + if (selectedRows.length === 0) { Swal.fire("알림", "선택된 데이터가 없습니다.", "warning"); return; } + const ids = collectWriteIds(); + if (ids.length === 0) { Swal.fire("알림", "작성중인 데이터만 삭제 가능합니다.", "warning"); return; } + + const confirm = await Swal.fire({ + title: "선택된 데이터를 삭제 하시겠습니까?", + icon: "warning", showCancelButton: true, + confirmButtonText: "확인", cancelButtonText: "취소", + }); + if (!confirm.isConfirmed) return; + + const res = await fetch("/api/work/diary/delete", { + method: "POST", headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ checkArr: ids }), + }); + const json = await res.json(); + await Swal.fire(json.msg || (json.success ? "삭제되었습니다." : "삭제 실패"), "", json.success ? "success" : "error"); + if (json.success) fetchData(); + }; + + const handleConfirm = async () => { + if (selectedRows.length === 0) { Swal.fire("알림", "선택된 데이터가 없습니다.", "warning"); return; } + const ids = collectWriteIds(); + if (ids.length === 0) { Swal.fire("알림", "등록중인 데이터만 배포 가능합니다.", "warning"); return; } + + const confirm = await Swal.fire({ + title: "선택된 데이터를 확정 하시겠습니까?", + icon: "warning", showCancelButton: true, + confirmButtonText: "확인", cancelButtonText: "취소", + }); + if (!confirm.isConfirmed) return; + + const res = await fetch("/api/work/diary/confirm", { + method: "POST", headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ checkArr: ids }), + }); + const json = await res.json(); + await Swal.fire(json.msg || (json.success ? "확정되었습니다." : "확정 실패"), "", json.success ? "success" : "error"); + if (json.success) fetchData(); + }; + + const columns: GridColumn[] = [ + { + title: "구분", field: "DIVISION", width: 110, hozAlign: "center", + formatter: (v) => v === "project" ? "프로젝트" : v === "non_project" ? "비프로젝트" : String(v ?? ""), + cellClick: (row) => openFormPopup(String(row.OBJID)), + }, + { title: "프로젝트번호", field: "PROJECT_NO", width: 130, hozAlign: "left" }, + { title: "유닛명", field: "UNIT_CODE_NAME", width: 260, hozAlign: "left" }, + { title: "TASK명", field: "TASK_NAME", width: 320, hozAlign: "left" }, + { title: "팀명", field: "DEPT_NAME", width: 110, hozAlign: "center" }, + { title: "작업자", field: "WORKER_NAME", width: 110, hozAlign: "center" }, + { title: "작업시작일", field: "WORK_START_DATE", width: 110, hozAlign: "center" }, + { title: "작업종료일", field: "WORK_END_DATE", width: 110, hozAlign: "center" }, + { title: "작업시간", field: "WORK_HOUR", width: 90, hozAlign: "right", formatter: "money" }, + { title: "상태", field: "STATUS_TITLE", width: 90, hozAlign: "center" }, + ]; + + return ( +
+
+

작업일지

+
+ + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+ ); +} diff --git a/src/app/(main)/work/status/page.tsx b/src/app/(main)/work/status/page.tsx new file mode 100644 index 0000000..098ce4c --- /dev/null +++ b/src/app/(main)/work/status/page.tsx @@ -0,0 +1,372 @@ +"use client"; + +import { Suspense, useCallback, useEffect, useMemo, useState } from "react"; +import { useSearchParams } from "next/navigation"; +import Swal from "sweetalert2"; +import { DataGrid, type GridColumn } from "@/components/grid/data-grid"; +import { SearchForm, SearchField } from "@/components/layout/search-form"; +import { Button } from "@/components/ui/button"; +import { SearchableSelect } from "@/components/ui/searchable-select"; +import { numberWithCommas } from "@/lib/utils"; + +// productionplanning/workStatusByProjectList.jsp + workStatusByImployeeList.jsp 대응 +// 탭: 프로젝트 작업현황 / 담당자별 작업현황 +type Tab = "project" | "employee"; + +interface Option { value: string; label: string } + +interface ProjectSumMap { + SUM_DESIGN_INPUT?: number; + SUM_PURCHASE_INPUT?: number; + SUM_SALES_INPUT?: number; + SUM_PRODUCTION_MGMT_INPUT?: number; + SUM_PRODUCTION_INPUT?: number; + SUM_MGMT_INPUT?: number; + SUM_OUTSOURCING?: number; + SUM_WORK_HOUR?: number; + SUM_MAN_DAY?: number; + SUM_MAN_MONTH?: number; +} + +interface EmployeeSumMap { + SUM_WORK_HOUR?: number; + SUM_MAN_DAY?: number; + SUM_MAN_MONTH?: number; +} + +export default function WorkStatusPage() { + return ( + 로딩 중...
}> + + + ); +} + +function WorkStatusContent() { + const searchParams = useSearchParams(); + const tabParam = searchParams.get("tab"); + const [tab, setTab] = useState(tabParam === "employee" ? "employee" : "project"); + + // 메뉴 재클릭 등으로 ?tab= 변경될 때 탭 동기화 + useEffect(() => { + if (tabParam === "employee" || tabParam === "project") setTab(tabParam); + }, [tabParam]); + + // 공용 드롭다운 옵션 + const [projectOptions, setProjectOptions] = useState([]); + const [deptOptions, setDeptOptions] = useState([]); + const [workerOptions, setWorkerOptions] = useState([]); + + useEffect(() => { + fetch("/api/common/project-list", { method: "POST", headers: { "Content-Type": "application/json" }, body: "{}" }) + .then((r) => r.json()) + .then((d) => { + const rows = (d.RESULTLIST || []) as Record[]; + setProjectOptions(rows.map((r) => ({ + value: String(r.OBJID ?? ""), + label: String(r.LABEL ?? r.PROJECT_NO ?? ""), + }))); + }) + .catch(() => setProjectOptions([])); + + fetch("/api/common/dept-list", { method: "POST", headers: { "Content-Type": "application/json" }, body: "{}" }) + .then((r) => r.json()) + .then((d) => { + const rows = (d.RESULTLIST || []) as Record[]; + setDeptOptions(rows.map((r) => ({ + value: String(r.DEPT_CODE ?? ""), + label: String(r.DEPT_NAME ?? ""), + }))); + }) + .catch(() => setDeptOptions([])); + + fetch("/api/common/user-list", { method: "POST", headers: { "Content-Type": "application/json" }, body: "{}" }) + .then((r) => r.json()) + .then((d) => { + const rows = (d.RESULTLIST || []) as Record[]; + setWorkerOptions(rows.map((r) => ({ + value: String(r.USER_ID ?? ""), + label: `${r.DEPT_NAME ?? ""} / ${r.USER_NAME ?? ""}`, + }))); + }) + .catch(() => setWorkerOptions([])); + }, []); + + return ( +
+

작업관리 현황

+ +
+ setTab("project")} label="프로젝트 작업현황" /> + setTab("employee")} label="담당자별 작업현황" /> +
+ + {tab === "project" ? ( + + ) : ( + + )} +
+ ); +} + +function TabButton({ active, onClick, label }: { active: boolean; onClick: () => void; label: string }) { + return ( + + ); +} + +// ── 프로젝트 작업현황 ──────────────────────────────────────── +function ProjectTab({ projectOptions }: { projectOptions: Option[] }) { + const [projectObjId, setProjectObjId] = useState(""); + const [unitCode, setUnitCode] = useState(""); + const [unitOptions, setUnitOptions] = useState([]); + const [rows, setRows] = useState[]>([]); + const [sumMap, setSumMap] = useState({}); + + const handleProjectChange = useCallback((next: string) => { + setProjectObjId(next); + setUnitCode(""); + if (!next) { setUnitOptions([]); return; } + fetch("/api/common/unit-list", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ contract_objid: next }), + }) + .then((r) => r.json()) + .then((d) => { + const list = (d.RESULTLIST || []) as Record[]; + setUnitOptions(list.map((r) => ({ + value: String(r.OBJID ?? ""), + label: String(r.UNIT_NAME ?? ""), + }))); + }) + .catch(() => setUnitOptions([])); + }, []); + + const fetchData = useCallback(async () => { + if (!projectObjId) { + Swal.fire("알림", "프로젝트번호는 필수값입니다.", "warning"); + return; + } + const res = await fetch("/api/work/status/project", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ project_nos: [projectObjId], unit_code: unitCode }), + }); + if (res.ok) { + const json = await res.json(); + setRows(json.RESULTLIST || []); + setSumMap(json.SUM_PRICE_MAP || {}); + } + }, [projectObjId, unitCode]); + + const columns: GridColumn[] = [ + { + title: "프로젝트", headerHozAlign: "center", + columns: [ + { title: "프로젝트번호", field: "PROJECT_NO", width: 140, hozAlign: "left" }, + { title: "유닛명", field: "UNIT_NAME", width: 200, hozAlign: "left" }, + ], + }, + { + title: "투입공수", headerHozAlign: "center", + columns: [ + { title: "영업", field: "SALES_INPUT", width: 80, hozAlign: "right", formatter: "money" }, + { title: "관리", field: "MGMT_INPUT", width: 80, hozAlign: "right", formatter: "money" }, + { title: "설계", field: "DESIGN_INPUT", width: 80, hozAlign: "right", formatter: "money" }, + { title: "구매", field: "PURCHASE_INPUT", width: 80, hozAlign: "right", formatter: "money" }, + { title: "생관", field: "PRODUCTION_MGMT_INPUT", width: 80, hozAlign: "right", formatter: "money" }, + { title: "생산", field: "PRODUCTION_INPUT", width: 80, hozAlign: "right", formatter: "money" }, + { title: "외주", field: "OUTSOURCING", width: 80, hozAlign: "right", formatter: "money" }, + { title: "작업시간(h)", field: "WORK_HOUR", width: 110, hozAlign: "right", formatter: "money" }, + { title: "Day/Man", field: "MAN_DAY", width: 90, hozAlign: "right", formatter: "money" }, + { title: "Month/Man", field: "MAN_MONTH", width: 100, hozAlign: "right", formatter: "money" }, + ], + }, + ]; + + return ( + <> + + + + + + + + + +
+ +
+ + + + {/* 합계 */} + {rows.length > 0 && ( +
+ (계) + + + + + + + + + + +
+ )} + + ); +} + +// ── 담당자별 작업현황 ──────────────────────────────────────── +function EmployeeTab({ projectOptions, deptOptions, workerOptions }: { + projectOptions: Option[]; deptOptions: Option[]; workerOptions: Option[]; +}) { + const [year, setYear] = useState(new Date().getFullYear().toString()); + const [projectObjId, setProjectObjId] = useState(""); + const [deptCode, setDeptCode] = useState(""); + const [worker, setWorker] = useState(""); + const [list, setList] = useState[]>([]); + const [npList, setNpList] = useState[]>([]); + const [sumMap, setSumMap] = useState({}); + + // 부서 선택 시 작업자 목록 필터링 + const filteredWorkers = useMemo(() => { + if (!deptCode) return workerOptions; + // user-list 옵션 label을 '부서명 / 작업자'로 구성하므로 간단 필터링 불가 → 전체 재요청 생략 + return workerOptions; + }, [deptCode, workerOptions]); + + const fetchData = useCallback(async () => { + if (!deptCode && !worker) { + Swal.fire("알림", "팀명 또는 작업자는 필수값입니다.", "warning"); + return; + } + const res = await fetch("/api/work/status/employee", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + year, + project_nos: projectObjId ? [projectObjId] : [], + busUsersDeptId: deptCode, + worker, + }), + }); + if (res.ok) { + const json = await res.json(); + setList(json.LIST || []); + setNpList(json.NP_LIST || []); + setSumMap(json.SUM_PRICE_MAP || {}); + } + }, [year, projectObjId, deptCode, worker]); + + // 합쳐서 그리드에 표시: 프로젝트 행 + 합계 구분행 + 비프로젝트 행 + const combined = useMemo(() => { + const projectRows = list.map((r) => ({ ...r, _DIVISION: "프로젝트" })); + const npRows = npList.map((r) => ({ ...r, _DIVISION: "비프로젝트", PROJECT_NO: "" })); + return [...projectRows, ...npRows]; + }, [list, npList]); + + const columns: GridColumn[] = [ + { title: "구분", field: "_DIVISION", width: 100, hozAlign: "left" }, + { title: "프로젝트번호", field: "PROJECT_NO", width: 140, hozAlign: "left" }, + { title: "팀명", field: "WORKER_DEPT_NAME", width: 120, hozAlign: "center" }, + { title: "작업자", field: "WORKER_USER_NAME", width: 120, hozAlign: "center" }, + { title: "작업시간", field: "WORK_HOUR", width: 100, hozAlign: "right", formatter: "money" }, + { title: "Day/Man", field: "MAN_DAY", width: 100, hozAlign: "right", formatter: "money" }, + { title: "Month/Man", field: "MAN_MONTH", width: 110, hozAlign: "right", formatter: "money" }, + ]; + + return ( + <> + + + + + + + + + + + + + + + +
+ +
+ + + + {/* 프로젝트 합계 */} + {list.length > 0 && ( +
+ 프로젝트 합계 + + + +
+ )} + + ); +} + +function SumCell({ label, value }: { label: string; value?: number }) { + return ( + + {label}:{" "} + {numberWithCommas(Number(value) || 0)} + + ); +} diff --git a/src/app/admin-panel/CLAUDE.md b/src/app/admin-panel/CLAUDE.md new file mode 100644 index 0000000..8eb8ead --- /dev/null +++ b/src/app/admin-panel/CLAUDE.md @@ -0,0 +1,24 @@ +## 역할 +관리자 전용 시스템 관리 패널. `window.open` 팝업으로 열리며, 사용자/부서/권한/메뉴/공통코드/공급업체 관리 및 시스템 로그 조회를 담당. + +## 주요 파일 +- **page.tsx** — 단일 파일(570줄+)에 모든 관리 탭 포함. 좌측 메뉴 + 우측 콘텐츠 2분할 레이아웃. + +## 공통 패턴 +- 탭 관리: `AdminTab` 타입으로 activeTab 제어 (user/code/menu/auth/dept 등) +- 각 탭별 인라인 컴포넌트: `UserManagement()`, `CodeManagement()` 등 +- 데이터 조회: `POST /api/admin/{resource}` → `json.RESULTLIST` +- 폼 팝업: `window.open("/admin-panel/{resource}-form?actionType=regist")` + +## 연결 고리 +- API: `/api/admin/users`, `/api/admin/codes`, `/api/admin/menus`, `/api/admin/auth`, `/api/admin/dept`, `/api/admin/supply`, `/api/admin/log-login`, `/api/admin/log-file` +- 컴포넌트: DataGrid, SearchForm, SearchField, Button, Input + +## 숨겨진 스펙 +- 좌측 메뉴: `ADMIN_MENUS` 배열로 정의, 섹션별 아코디언(openSections Set) +- 활성 탭: 파란 배경(`bg-[#1C90FB]`), 비활성: 회색 +- 미구현 탭: `PlaceholderContent` 준비 중 메시지 표시 +- 폼 팝업 경로: 신규 `?actionType=regist`, 수정 `?{id}=${id}` +- 검색: 텍스트 Input + 상태 select(active/inActive) 조합 + +@MISTAKES.md diff --git a/src/app/admin-panel/MISTAKES.md b/src/app/admin-panel/MISTAKES.md new file mode 100644 index 0000000..cff49f8 --- /dev/null +++ b/src/app/admin-panel/MISTAKES.md @@ -0,0 +1,9 @@ +# admin-panel 오답노트 + + diff --git a/src/app/admin-panel/code-form/page.tsx b/src/app/admin-panel/code-form/page.tsx new file mode 100644 index 0000000..94641db --- /dev/null +++ b/src/app/admin-panel/code-form/page.tsx @@ -0,0 +1,200 @@ +"use client"; + +import { useState, useEffect, Suspense } from "react"; +import { useSearchParams } from "next/navigation"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import Swal from "sweetalert2"; + +interface ParentCode { CODE_ID: string; CODE_NAME: string } + +function CodeForm() { + const searchParams = useSearchParams(); + const codeId = searchParams.get("codeId"); + const isNew = !codeId; + const [form, setForm] = useState>({}); + const [parents, setParents] = useState([]); + const [loading, setLoading] = useState(false); + const [parentOpen, setParentOpen] = useState(false); + const [parentSearch, setParentSearch] = useState(""); + const set = (k: string, v: string) => setForm((p) => ({ ...p, [k]: v })); + + const selectedParent = parents.find((p) => p.CODE_ID === (form.parent_code_id || "")); + const filteredParents = parentSearch + ? parents.filter( + (p) => + p.CODE_NAME.toLowerCase().includes(parentSearch.toLowerCase()) || + p.CODE_ID.toLowerCase().includes(parentSearch.toLowerCase()) + ) + : parents; + + // 분류(최상위) 코드 로드 + useEffect(() => { + fetch("/api/admin/codes", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ parentOnly: true }), + }) + .then((r) => r.json()) + .then((d) => { + const rows = (d.RESULTLIST || []) as Array>; + setParents(rows.map((r) => ({ CODE_ID: String(r.CODE_ID || ""), CODE_NAME: String(r.CODE_NAME || "") }))); + }) + .catch(() => {}); + }, []); + + // 수정 시 상세 로드 + useEffect(() => { + if (codeId) { + fetch("/api/admin/codes/detail", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ codeId }), + }) + .then((r) => r.json()) + .then((d) => { + if (d.success) setForm(d.data); + }) + .catch(() => {}); + } + }, [codeId]); + + const handleSave = async () => { + if (!form.code_name) { + Swal.fire("알림", "코드명을 입력하세요.", "warning"); + return; + } + setLoading(true); + try { + const res = await fetch("/api/admin/codes/save", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ ...form, actionType: isNew ? "regist" : "update" }), + }); + const data = await res.json(); + if (data.success) { + await Swal.fire({ icon: "success", title: data.message || "저장되었습니다.", timer: 1500, showConfirmButton: false }); + if (window.opener) { + try { window.opener.location.reload(); } catch {} + } + if (isNew) window.close(); + } else { + Swal.fire("오류", data.message || "저장 실패", "error"); + } + } catch { + Swal.fire("오류", "서버 오류", "error"); + } finally { + setLoading(false); + } + }; + + return ( +
+

+ 공통코드 관리 +

+
+ + + + + + + + + + + + + + + +
분류선택 +
+ { setParentOpen(true); setParentSearch(""); }} + onBlur={() => setTimeout(() => setParentOpen(false), 150)} + onChange={(e) => setParentSearch(e.target.value)} + className="h-9 w-full rounded border border-gray-300 bg-white pl-2 pr-8 text-sm outline-none focus:ring-1 focus:ring-blue-400" + /> + {selectedParent && !parentOpen && ( + + )} + {parentOpen && ( +
+ + {filteredParents.length === 0 ? ( +
결과 없음
+ ) : ( + filteredParents.map((p) => ( + + )) + )} +
+ )} +
+
+ 코드명 * + + set("code_name", e.target.value)} /> +
활성화여부 + +
+
+
+ + +
+
+ ); +} + +export default function Page() { + return ( + 로딩 중...
}> + + + ); +} diff --git a/src/app/admin-panel/page.tsx b/src/app/admin-panel/page.tsx new file mode 100644 index 0000000..49879b6 --- /dev/null +++ b/src/app/admin-panel/page.tsx @@ -0,0 +1,1645 @@ +"use client"; + +import { useState, useCallback, useEffect, useMemo } from "react"; +import { cn } from "@/lib/utils"; +import { DataGrid, type GridColumn } from "@/components/grid/data-grid"; +import { SearchForm, SearchField } from "@/components/layout/search-form"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { CodeSelect } from "@/components/ui/code-select"; +import Swal from "sweetalert2"; +import { + Menu, Shield, Users, Database, FileText, Activity, + ChevronRight, Settings, +} from "lucide-react"; + +// admin/adminMainFS.do 대응 - 관리자 팝업 페이지 +type AdminTab = "menu" | "auth" | "user" | "dept" | "code" | "supply" | "template" | "exchange" | "log-file" | "log-login" | "log-mail" | "ref-customer" | "ref-material" | "ref-car" | "ref-car-grade" | "ref-product-group" | "ref-product" | "spec-data-category" | "car-option"; + +const ADMIN_MENUS = [ + { + label: "메뉴관리", icon: Menu, + items: [{ key: "menu" as AdminTab, label: "메뉴관리" }], + }, + { + label: "권한 및 사용자 관리", icon: Shield, + items: [ + { key: "auth" as AdminTab, label: "권한 관리" }, + { key: "dept" as AdminTab, label: "부서 관리" }, + { key: "user" as AdminTab, label: "사용자 관리" }, + ], + }, + { + label: "기준정보관리", icon: Database, + items: [ + { key: "code" as AdminTab, label: "공통코드관리" }, + { key: "supply" as AdminTab, label: "공급업체관리" }, + { key: "template" as AdminTab, label: "템플릿 관리" }, + { key: "exchange" as AdminTab, label: "환율관리" }, + ], + }, + { + label: "기준정보 관리 (상세)", icon: Settings, + items: [ + { key: "ref-customer" as AdminTab, label: "고객사 관리" }, + { key: "ref-material" as AdminTab, label: "재질 관리" }, + { key: "ref-car" as AdminTab, label: "차종 관리" }, + { key: "ref-car-grade" as AdminTab, label: "차종 Grade 관리" }, + { key: "ref-product-group" as AdminTab, label: "제품군 관리" }, + { key: "ref-product" as AdminTab, label: "제품 관리" }, + { key: "spec-data-category" as AdminTab, label: "기술자료 카테고리 관리" }, + { key: "car-option" as AdminTab, label: "차량옵션(사양) 관리" }, + ], + }, + { + label: "System Log", icon: Activity, + items: [ + { key: "log-file" as AdminTab, label: "파일 다운로드 로그" }, + { key: "log-login" as AdminTab, label: "로그인 로그" }, + { key: "log-mail" as AdminTab, label: "메일발송 로그" }, + ], + }, +]; + +export default function AdminPanelPage() { + const [activeTab, setActiveTab] = useState("user"); + const [openSections, setOpenSections] = useState>(new Set(["권한 및 사용자 관리"])); + + const toggleSection = (label: string) => { + setOpenSections((prev) => { + const next = new Set(prev); + if (next.has(label)) next.delete(label); + else next.add(label); + return next; + }); + }; + + return ( +
+ {/* 좌측 메뉴 (adminMenu.jsp 대응) */} + + + {/* 우측 콘텐츠 */} +
+ {activeTab === "user" && } + {activeTab === "code" && } + {activeTab === "menu" && } + {activeTab === "auth" && } + {activeTab === "dept" && } + {activeTab === "supply" && } + {activeTab === "log-login" && } + {activeTab === "log-file" && } + {activeTab === "log-mail" && } + {activeTab === "template" && } + {activeTab === "exchange" && } + {activeTab === "ref-customer" && } + {activeTab === "ref-material" && } + {activeTab === "ref-car" && } + {activeTab === "ref-product-group" && } + {activeTab === "ref-product" && } + {activeTab === "spec-data-category" && } + {activeTab === "car-option" && } + {/* 기타 탭은 공통 Placeholder (DB 테이블 없음) */} + {!["user","code","menu","auth","dept","supply","log-login","log-file","log-mail","template","exchange","ref-customer","ref-material","ref-car","ref-product-group","ref-product","spec-data-category","car-option"].includes(activeTab) && ( + s.items).find(i => i.key === activeTab)?.label || activeTab} /> + )} +
+
+ ); +} + +// ========================================== +// 사용자 관리 (admin/user/userList.jsp 대응) +// ========================================== +function UserManagement() { + const [searchName, setSearchName] = useState(""); + const [searchDept, setSearchDept] = useState(""); + const [searchType, setSearchType] = useState(""); + const [data, setData] = useState[]>([]); + + const columns: GridColumn[] = [ + { title: "부서명", field: "DEPT_NAME", width: 120 }, + { title: "사용자 ID", field: "USER_ID", width: 120, cellClick: (row) => openUserForm(String(row.USER_ID)) }, + { title: "사용자명", field: "USER_NAME", width: 100, hozAlign: "center" }, + { title: "전화번호", field: "CELL_PHONE", width: 130 }, + { title: "이메일", field: "EMAIL", width: 180 }, + { title: "등록일", field: "REGDATE", width: 100, hozAlign: "center" }, + { title: "상태", field: "STATUS", width: 80, hozAlign: "center" }, + ]; + + const fetchData = useCallback(async () => { + const res = await fetch("/api/admin/users", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ user_name: searchName, dept_name: searchDept, user_type: searchType }), + }); + if (res.ok) { const json = await res.json(); setData(json.RESULTLIST || []); } + }, [searchName, searchDept, searchType]); + + useEffect(() => { fetchData(); }, [fetchData]); + + const openUserForm = (userId?: string) => { + window.open( + `/admin-panel/user-form${userId ? `?userId=${userId}` : "?actionType=regist"}`, + "userFormPopup", "width=500,height=620" + ); + }; + + const handleDelete = async () => { + Swal.fire({ icon: "warning", title: "삭제할 사용자를 선택하세요." }); + }; + + return ( +
+

사용자 관리

+ + + + + + setSearchDept(e.target.value)} className="w-[120px]" /> + + + setSearchName(e.target.value)} className="w-[120px]" /> + + +
+ + + +
+ +
+ ); +} + +// ========================================== +// 공통코드 관리 (admin/codeCategory/codeCategoryMngList.jsp) +// ========================================== +interface CodeRow { + OBJID: string; + CODE_ID: string; + CODE_NAME: string; + EXT_VAL: string; + PARENT_CODE_ID: string; + WRITER_NAME: string; + REGDATE: string; + STATUS: string; +} + +function CodeManagement() { + const [searchCode, setSearchCode] = useState(""); + const [searchStatus, setSearchStatus] = useState("active"); + const [data, setData] = useState([]); + const [expanded, setExpanded] = useState>(new Set()); + + const fetchData = useCallback(async () => { + const res = await fetch("/api/admin/codes", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ code_name: searchCode, status: searchStatus }), + }); + if (res.ok) { + const json = await res.json(); + const rows = (json.RESULTLIST || []) as Record[]; + const mapped: CodeRow[] = rows.map((r) => ({ + OBJID: String(r.OBJID || ""), + CODE_ID: String(r.CODE_ID || ""), + CODE_NAME: String(r.CODE_NAME || ""), + EXT_VAL: String(r.EXT_VAL || ""), + PARENT_CODE_ID: String(r.PARENT_CODE_ID || ""), + WRITER_NAME: String(r.WRITER_NAME || ""), + REGDATE: String(r.REGDATE || ""), + STATUS: String(r.STATUS || ""), + })); + setData(mapped); + } + }, [searchCode, searchStatus]); + + useEffect(() => { fetchData(); }, [fetchData]); + + // 검색어가 있으면 평면 리스트, 없으면 트리 모드 + const isSearching = searchCode.trim().length > 0; + + // code_id → 자식 리스트 매핑 + const childrenMap = useMemo(() => { + const m = new Map(); + data.forEach((r) => { + const key = r.PARENT_CODE_ID || ""; + if (!m.has(key)) m.set(key, []); + m.get(key)!.push(r); + }); + return m; + }, [data]); + + // 표시 행 (트리 평탄화 또는 검색 평면) + const visibleRows = useMemo(() => { + if (isSearching) { + return data.map((r) => ({ row: r, depth: 0, hasChildren: false })); + } + const out: { row: CodeRow; depth: number; hasChildren: boolean }[] = []; + const walk = (parentId: string, depth: number) => { + const kids = childrenMap.get(parentId) || []; + kids.forEach((k) => { + const hasChildren = (childrenMap.get(k.CODE_ID) || []).length > 0; + out.push({ row: k, depth, hasChildren }); + if (expanded.has(k.CODE_ID)) walk(k.CODE_ID, depth + 1); + }); + }; + walk("", 0); + return out; + }, [data, childrenMap, expanded, isSearching]); + + const toggleExpand = (codeId: string) => { + setExpanded((prev) => { + const next = new Set(prev); + if (next.has(codeId)) next.delete(codeId); else next.add(codeId); + return next; + }); + }; + + const expandAll = () => setExpanded(new Set(data.map((r) => r.CODE_ID))); + const collapseAll = () => setExpanded(new Set()); + + const openCodeDetail = (codeId: string) => { + window.open(`/admin-panel/code-form?codeId=${codeId}`, "codeFormPopup", "width=500,height=600"); + }; + + return ( +
+

공통코드관리

+ + + setSearchCode(e.target.value)} className="w-[150px]" /> + + + + + +
+ {!isSearching && ( + <> + + + + )} + + +
+
+ 총 {data.length}건{isSearching ? " (검색 결과 - 평면 리스트)" : ` · 트리 모드 (최상위 ${(childrenMap.get("") || []).length}개)`} +
+
+ + + + + + + + + + + + + {visibleRows.map(({ row, depth, hasChildren }) => { + const isExp = expanded.has(row.CODE_ID); + return ( + openCodeDetail(row.CODE_ID)} + > + + + + + + + + ); + })} + {visibleRows.length === 0 && ( + + )} + +
ID코드명CODE NO등록자등록일활성화
{row.CODE_ID} +
+ {!isSearching && hasChildren ? ( + + ) : ( + · + )} + + {row.CODE_NAME} + +
+
{row.EXT_VAL || "-"}{row.WRITER_NAME || "-"}{row.REGDATE} + {row.STATUS === "active" ? ( + 활성 + ) : ( + 비활성 + )} +
데이터가 없습니다.
+
+
+ ); +} + +// ========================================== +// 메뉴 관리 (트리구조 CRUD) +// ========================================== +interface MenuNode { + OBJID: string; MENU_NAME_KOR: string; MENU_NAME_ENG: string; + MENU_URL: string; PARENT_OBJ_ID: string; SEQ: string; STATUS: string; + children: MenuNode[]; +} + +function MenuManagement() { + const [tree, setTree] = useState([]); + const [flatAll, setFlatAll] = useState([]); + const [expanded, setExpanded] = useState>(new Set()); + const [selected, setSelected] = useState(null); + const [editForm, setEditForm] = useState>({}); + const [saving, setSaving] = useState(false); + const [showForm, setShowForm] = useState(false); + + const fetchData = useCallback(async () => { + const res = await fetch("/api/admin/menus", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({}) }); + if (!res.ok) return; + const json = await res.json(); + const flat: MenuNode[] = (json.RESULTLIST || []).map((r: Record) => ({ + OBJID: String(r.OBJID || ""), MENU_NAME_KOR: String(r.MENU_NAME_KOR || ""), + MENU_NAME_ENG: String(r.MENU_NAME_ENG || ""), MENU_URL: String(r.MENU_URL || ""), + PARENT_OBJ_ID: String(r.PARENT_OBJ_ID || "0"), SEQ: String(r.SEQ || "0"), + STATUS: String(r.STATUS || ""), children: [], + })); + const map = new Map(); + flat.forEach((n) => map.set(n.OBJID, n)); + const roots: MenuNode[] = []; + flat.forEach((n) => { + const parent = map.get(n.PARENT_OBJ_ID); + if (parent) parent.children.push(n); + else roots.push(n); + }); + const sortNodes = (nodes: MenuNode[]) => { + nodes.sort((a, b) => parseInt(a.SEQ || "0") - parseInt(b.SEQ || "0")); + nodes.forEach((n) => sortNodes(n.children)); + }; + sortNodes(roots); + setTree(roots); + setFlatAll(flat); + setExpanded(new Set(roots.map((r) => r.OBJID))); + }, []); + + useEffect(() => { fetchData(); }, [fetchData]); + + const parentNameMap = useMemo(() => { + const m = new Map(); + flatAll.forEach((n) => m.set(n.OBJID, n.MENU_NAME_KOR)); + return m; + }, [flatAll]); + + // 트리를 확장 상태에 맞게 평탄화 (depth 포함) + const visibleRows = useMemo(() => { + const out: { node: MenuNode; depth: number; hasChildren: boolean }[] = []; + const walk = (nodes: MenuNode[], depth: number) => { + nodes.forEach((n) => { + const hasChildren = n.children.length > 0; + out.push({ node: n, depth, hasChildren }); + if (expanded.has(n.OBJID)) walk(n.children, depth + 1); + }); + }; + walk(tree, 0); + return out; + }, [tree, expanded]); + + const toggleExpand = (id: string) => { + setExpanded((prev) => { + const next = new Set(prev); + if (next.has(id)) next.delete(id); else next.add(id); + return next; + }); + }; + + const expandAll = () => setExpanded(new Set(flatAll.map((n) => n.OBJID))); + const collapseAll = () => setExpanded(new Set(tree.map((r) => r.OBJID))); + + const openEdit = (node: MenuNode) => { + setSelected(node); + setEditForm({ + objid: node.OBJID, menu_name_kor: node.MENU_NAME_KOR, + menu_name_eng: node.MENU_NAME_ENG, menu_url: node.MENU_URL, + parent_obj_id: node.PARENT_OBJ_ID, seq: node.SEQ, status: node.STATUS, + }); + setShowForm(true); + }; + + const handleNew = (parentId: string = "0") => { + setSelected(null); + setEditForm({ menu_name_kor: "", menu_name_eng: "", menu_url: "", parent_obj_id: parentId, seq: "0", status: "active", actionType: "regist" }); + setShowForm(true); + }; + + const handleSave = async () => { + if (!editForm.menu_name_kor) { Swal.fire("알림", "메뉴명을 입력하세요.", "warning"); return; } + setSaving(true); + try { + const res = await fetch("/api/admin/menus/save", { + method: "POST", headers: { "Content-Type": "application/json" }, + body: JSON.stringify(editForm), + }); + const data = await res.json(); + if (data.success) { + Swal.fire({ icon: "success", title: data.message, timer: 1200, showConfirmButton: false }); + setShowForm(false); + fetchData(); + } else { Swal.fire("오류", data.message, "error"); } + } catch { Swal.fire("오류", "서버 오류", "error"); } + finally { setSaving(false); } + }; + + const handleDelete = async () => { + if (!selected) { Swal.fire("알림", "삭제할 메뉴를 선택하세요.", "warning"); return; } + const r = await Swal.fire({ title: "삭제 확인", text: `"${selected.MENU_NAME_KOR}" 메뉴를 삭제하시겠습니까?`, icon: "warning", showCancelButton: true, confirmButtonText: "삭제", cancelButtonText: "취소" }); + if (!r.isConfirmed) return; + const res = await fetch("/api/admin/menus/delete", { + method: "POST", headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ objid: selected.OBJID }), + }); + const data = await res.json(); + if (data.success) { + Swal.fire({ icon: "success", title: "삭제되었습니다.", timer: 1200, showConfirmButton: false }); + setSelected(null); setEditForm({}); setShowForm(false); + fetchData(); + } else { Swal.fire("오류", data.message, "error"); } + }; + + const set = (k: string, v: string) => setEditForm((p) => ({ ...p, [k]: v })); + + return ( +
+

메뉴 관리

+
+ + + + +
+ +
+ + + + + + + + + + + + + {visibleRows.map(({ node, depth, hasChildren }) => { + const isSel = selected?.OBJID === node.OBJID; + const isExp = expanded.has(node.OBJID); + const parentName = node.PARENT_OBJ_ID === "0" ? "-" : parentNameMap.get(node.PARENT_OBJ_ID) || "-"; + return ( + openEdit(node)} + > + + + + + + + + ); + })} + {visibleRows.length === 0 && ( + + )} + +
메뉴명상위메뉴URL순서활성화작업
+
+ {hasChildren ? ( + + ) : ( + · + )} + + {node.MENU_NAME_KOR} + +
+
{parentName}{node.MENU_URL || "-"}{node.SEQ} + {node.STATUS === "active" ? ( + 활성 + ) : ( + 비활성 + )} + + +
메뉴가 없습니다.
+
+ + {showForm && ( +
+
+

+ {editForm.actionType === "regist" ? "메뉴 등록" : `메뉴 수정 — ${editForm.menu_name_kor}`} +

+
+
+ set("menu_name_kor", e.target.value)} />
+
+ set("menu_name_eng", e.target.value)} />
+
+ set("menu_url", e.target.value)} placeholder="/module/page.do" />
+
+ set("seq", e.target.value)} />
+
+
+
+
+ {editForm.objid && ( +
+
+ )} +
+
+ + {selected && } + +
+
+
+ )} +
+ ); +} + +// ========================================== +// 권한 관리 (authMngList.jsp 대응) +// ========================================== +function AuthManagement() { + const [data, setData] = useState[]>([]); + const [selectedRows, setSelectedRows] = useState[]>([]); + // 등록/수정 폼 + const [showForm, setShowForm] = useState(false); + const [editForm, setEditForm] = useState>({}); + // 멤버 관리 + const [showMembers, setShowMembers] = useState(false); + const [memberTarget, setMemberTarget] = useState<{ objid: string; name: string } | null>(null); + const [members, setMembers] = useState[]>([]); + const [availableUsers, setAvailableUsers] = useState[]>([]); + const [selectedMembers, setSelectedMembers] = useState>(new Set()); + const [selectedUsers, setSelectedUsers] = useState>(new Set()); + const [userSearch, setUserSearch] = useState({ userName: "", deptName: "" }); + + const columns: GridColumn[] = [ + { title: "권한그룹명", field: "AUTH_NAME", width: 200, cellClick: (row) => openDetail(row) }, + { title: "권한CODE", field: "AUTH_CODE", width: 120 }, + { title: "사용자수", field: "USER_CNT", width: 100, hozAlign: "center", cellClick: (row) => openMembers(row) }, + { title: "등록자", field: "WRITER", width: 100, hozAlign: "center" }, + { title: "등록일", field: "REGDATE", width: 120, hozAlign: "center" }, + { title: "상태", field: "STATUS", width: 80, hozAlign: "center" }, + ]; + + const fetchData = useCallback(async () => { + const res = await fetch("/api/admin/auth", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({}) }); + if (res.ok) { const json = await res.json(); setData(json.RESULTLIST || []); } + }, []); + + useEffect(() => { fetchData(); }, [fetchData]); + + const openDetail = (row: Record) => { + setEditForm({ objid: String(row.OBJID || ""), auth_name: String(row.AUTH_NAME || ""), auth_code: String(row.AUTH_CODE || ""), status: String(row.STATUS || "active") }); + setShowForm(true); + setShowMembers(false); + }; + + const openNew = () => { + setEditForm({ auth_name: "", auth_code: "", status: "active", actionType: "regist" }); + setShowForm(true); + setShowMembers(false); + }; + + const handleSave = async () => { + if (!editForm.auth_name) { Swal.fire("알림", "권한명을 입력하세요.", "warning"); return; } + const res = await fetch("/api/admin/auth/save", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(editForm) }); + const json = await res.json(); + if (json.success) { Swal.fire({ icon: "success", title: json.message, timer: 1200, showConfirmButton: false }); setShowForm(false); fetchData(); } + else Swal.fire("오류", json.message, "error"); + }; + + const handleDelete = async (objid: string, name: string) => { + const r = await Swal.fire({ title: "삭제 확인", text: `"${name}" 권한그룹을 삭제하시겠습니까?`, icon: "warning", showCancelButton: true, confirmButtonText: "삭제", cancelButtonText: "취소" }); + if (!r.isConfirmed) return; + const res = await fetch("/api/admin/auth/delete", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ objid }) }); + const json = await res.json(); + if (json.success) { Swal.fire({ icon: "success", title: json.message, timer: 1200, showConfirmButton: false }); setShowForm(false); fetchData(); } + else Swal.fire("오류", json.message, "error"); + }; + + const handleStatusChange = async (status: string) => { + const objids = selectedRows.map((r) => r.OBJID).filter(Boolean); + if (objids.length === 0) { Swal.fire("알림", "대상을 선택하세요.", "warning"); return; } + const res = await fetch("/api/admin/auth/status", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ objids, status }) }); + const json = await res.json(); + if (json.success) { Swal.fire({ icon: "success", title: json.message, timer: 1200, showConfirmButton: false }); setSelectedRows([]); fetchData(); } + }; + + // === 멤버 관리 === + const openMembers = async (row: Record) => { + const objid = String(row.OBJID || ""); + const name = String(row.AUTH_NAME || ""); + setMemberTarget({ objid, name }); + setShowMembers(true); + setShowForm(false); + setSelectedMembers(new Set()); + setSelectedUsers(new Set()); + // 소속 멤버 조회 + const res = await fetch("/api/admin/auth/members", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ masterObjid: objid }) }); + if (res.ok) { const json = await res.json(); setMembers(json.RESULTLIST || []); } + }; + + const searchAvailableUsers = async () => { + if (!memberTarget) return; + const res = await fetch("/api/admin/auth/users", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ ...userSearch, masterObjid: memberTarget.objid }) }); + if (res.ok) { const json = await res.json(); setAvailableUsers(json.RESULTLIST || []); } + }; + + const addMembers = async () => { + if (!memberTarget) return; + const userIds = Array.from(selectedUsers).map((i) => availableUsers[i]?.USER_ID).filter(Boolean); + if (userIds.length === 0) { Swal.fire("알림", "추가할 사용자를 선택하세요.", "warning"); return; } + const res = await fetch("/api/admin/auth/members/save", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ masterObjid: memberTarget.objid, userIds }) }); + const json = await res.json(); + if (json.success) { Swal.fire({ icon: "success", title: json.message, timer: 1200, showConfirmButton: false }); openMembers({ OBJID: memberTarget.objid, AUTH_NAME: memberTarget.name }); searchAvailableUsers(); fetchData(); } + }; + + const removeMembers = async () => { + if (!memberTarget) return; + const memberObjids = Array.from(selectedMembers).map((i) => members[i]?.OBJID).filter(Boolean); + if (memberObjids.length === 0) { Swal.fire("알림", "제거할 멤버를 선택하세요.", "warning"); return; } + const res = await fetch("/api/admin/auth/members/delete", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ masterObjid: memberTarget.objid, memberObjids }) }); + const json = await res.json(); + if (json.success) { Swal.fire({ icon: "success", title: json.message, timer: 1200, showConfirmButton: false }); openMembers({ OBJID: memberTarget.objid, AUTH_NAME: memberTarget.name }); searchAvailableUsers(); fetchData(); } + }; + + const set = (k: string, v: string) => setEditForm((p) => ({ ...p, [k]: v })); + + return ( +
+

권한 관리

+
+ + + + +
+ + + {/* 등록/수정 폼 */} + {showForm && ( +
+
+

{editForm.actionType === "regist" ? "권한그룹 등록" : "권한그룹 수정"}

+
+
+ set("auth_name", e.target.value)} />
+
+ set("auth_code", e.target.value)} />
+
+
+
+
+ + {editForm.objid && } + +
+
+
+ )} + + {/* 멤버 관리 */} + {showMembers && memberTarget && ( +
+
+
+

소속인원 관리 — {memberTarget.name}

+ +
+
+ {/* 좌: 소속 멤버 */} +
+
소속 멤버 ({members.length}명)
+
+ + + + + + {members.map((m, i) => ( + + + + + + + ))} + {members.length === 0 && } + +
{ + setSelectedMembers(e.target.checked ? new Set(members.map((_, i) => i)) : new Set()); + }} />부서이름ID
{ const s = new Set(selectedMembers); if (e.target.checked) s.add(i); else s.delete(i); setSelectedMembers(s); }} />{String(m.DEPT_NAME || "")}{String(m.USER_NAME || "")}{String(m.USER_ID || "")}
소속 멤버가 없습니다.
+
+
+ + {/* 중: 이동 버튼 */} +
+ + +
+ + {/* 우: 미소속 사용자 */} +
+
사용자 검색
+
+ setUserSearch((p) => ({ ...p, deptName: e.target.value }))} /> + setUserSearch((p) => ({ ...p, userName: e.target.value }))} /> + +
+
+ + + + + + {availableUsers.map((u, i) => ( + + + + + + + ))} + {availableUsers.length === 0 && } + +
{ + setSelectedUsers(e.target.checked ? new Set(availableUsers.map((_, i) => i)) : new Set()); + }} />부서이름ID
{ const s = new Set(selectedUsers); if (e.target.checked) s.add(i); else s.delete(i); setSelectedUsers(s); }} />{String(u.DEPT_NAME || "")}{String(u.USER_NAME || "")}{String(u.USER_ID || "")}
조회 버튼을 클릭하세요.
+
+
+
+
+
+ )} +
+ ); +} + +// ========================================== +// 부서 관리 +// ========================================== +function DeptManagement() { + const [data, setData] = useState[]>([]); + const [showForm, setShowForm] = useState(false); + const [editForm, setEditForm] = useState>({}); + const [saving, setSaving] = useState(false); + + const fetchData = useCallback(async () => { + const res = await fetch("/api/admin/dept", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({}) }); + if (res.ok) { const json = await res.json(); setData(json.RESULTLIST || []); } + }, []); + + useEffect(() => { fetchData(); }, [fetchData]); + + const openNew = () => { + setEditForm({ actionType: "regist", dept_code: "", dept_name: "", company_name: "", status: "" }); + setShowForm(true); + }; + + const openEdit = (row: Record) => { + setEditForm({ + actionType: "update", + dept_code: String(row.DEPT_CODE || ""), + dept_name: String(row.DEPT_NAME || ""), + company_name: String(row.COMPANY_NAME || ""), + status: String(row.STATUS || "active"), + }); + setShowForm(true); + }; + + const columns: GridColumn[] = [ + { title: "부서코드", field: "DEPT_CODE", width: 140, cellClick: (row) => openEdit(row) }, + { title: "회사명", field: "COMPANY_NAME", width: 180 }, + { title: "부서명", field: "DEPT_NAME", width: 240 }, + { title: "활성화 여부", field: "STATUS_NAME", width: 100 }, + { title: "등록일", field: "REGDATE", width: 110, hozAlign: "center" }, + ]; + + const set = (k: string, v: string) => setEditForm((p) => ({ ...p, [k]: v })); + + const handleSave = async () => { + if (!editForm.dept_code || !editForm.dept_name) { + Swal.fire("알림", "부서코드와 부서명은 필수입니다.", "warning"); + return; + } + setSaving(true); + try { + const res = await fetch("/api/admin/dept/save", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(editForm), + }); + const json = await res.json(); + if (json.success) { + Swal.fire({ icon: "success", title: json.message, timer: 1200, showConfirmButton: false }); + setShowForm(false); + fetchData(); + } else { + Swal.fire("오류", json.message || "저장 실패", "error"); + } + } catch { + Swal.fire("오류", "서버 오류", "error"); + } finally { + setSaving(false); + } + }; + + return ( +
+

부서 관리

+
+ + +
+ + + {showForm && ( +
+
+

+ {editForm.actionType === "regist" ? "부서 등록" : `부서 수정 — ${editForm.dept_code}`} +

+ + + + + + + + + + + + + + + + + + + +
부서코드 * + set("dept_code", e.target.value)} + readOnly={editForm.actionType !== "regist"} + className={editForm.actionType !== "regist" ? "bg-gray-50" : ""} + /> +
회사명 + set("company_name", e.target.value)} /> +
부서명 * + set("dept_name", e.target.value)} /> +
활성화 여부 + +
+
+ + +
+
+
+ )} +
+ ); +} + +// ========================================== +// 공급업체 관리 +// ========================================== +function SupplyManagement() { + const [searchCode, setSearchCode] = useState(""); + const [searchName, setSearchName] = useState(""); + const [data, setData] = useState[]>([]); + + const openSupplyPopup = (objid: string = "") => { + const q = objid ? `?objid=${encodeURIComponent(objid)}` : ""; + window.open(`/admin-panel/supply-form${q}`, "supplyFormPopup", "width=1300,height=780"); + }; + + const columns: GridColumn[] = [ + { title: "구분", field: "SUPPLY_CODE_NAME", width: 100 }, + { title: "고객명", field: "SUPPLY_NAME", width: 200, cellClick: (row) => openSupplyPopup(String(row.OBJID || "")) }, + { title: "지역", field: "AREA_CD_NAME", width: 110 }, + { title: "대표자명", field: "CHARGE_USER_NAME", width: 100 }, + { title: "업종", field: "SUPPLY_STOCKNAME", width: 220 }, + { title: "업태", field: "SUPPLY_BUSNAME", width: 160 }, + { title: "주소", field: "SUPPLY_ADDRESS", width: 260 }, + { title: "핸드폰", field: "SUPPLY_TEL_NO", width: 140 }, + { title: "등록일", field: "REGDATE", width: 110, hozAlign: "center" }, + ]; + + const fetchData = useCallback(async () => { + const res = await fetch("/api/admin/supply", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ supply_code: searchCode, supply_name: searchName }), + }); + if (res.ok) { + const json = await res.json(); + setData(json.RESULTLIST || []); + } + }, [searchCode, searchName]); + + useEffect(() => { fetchData(); }, [fetchData]); + + return ( +
+

공급업체관리

+ + + setSearchCode(v)} className="w-[180px]" /> + + + setSearchName(e.target.value)} className="w-[200px]" /> + + +
+ + +
+
총 {data.length}건
+ +
+ ); +} + +// ========================================== +// 로그인 로그 +// ========================================== +function LoginLogPage() { + const [data, setData] = useState[]>([]); + const columns: GridColumn[] = [ + { title: "로그시간", field: "LOG_TIME", width: 160 }, + { title: "사용자ID", field: "USER_ID", width: 120 }, + { title: "결과", field: "LOGIN_RESULT", width: 80, hozAlign: "center" }, + { title: "IP주소", field: "REMOTE_ADDR", width: 130 }, + { title: "에러메시지", field: "ERROR_MESSAGE", width: 250 }, + ]; + const fetchData = useCallback(async () => { + const res = await fetch("/api/admin/log-login", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({}) }); + if (res.ok) { const json = await res.json(); setData(json.RESULTLIST || []); } + }, []); + + useEffect(() => { fetchData(); }, [fetchData]); + return ( +
+

로그인 로그

+
+ +
+ +
+ ); +} + +// ========================================== +// 파일 다운로드 로그 +// ========================================== +function FileDownloadLogPage() { + const [data, setData] = useState[]>([]); + const columns: GridColumn[] = [ + { title: "다운로드시간", field: "LOG_TIME", width: 160 }, + { title: "사용자", field: "USER_NAME", width: 100 }, + { title: "파일명", field: "REAL_FILE_NAME", width: 250 }, + { title: "문서유형", field: "DOC_TYPE_NAME", width: 150 }, + { title: "IP주소", field: "REMOTE_ADDR", width: 130 }, + ]; + const fetchData = useCallback(async () => { + const res = await fetch("/api/admin/log-file", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({}) }); + if (res.ok) { const json = await res.json(); setData(json.RESULTLIST || []); } + }, []); + + useEffect(() => { fetchData(); }, [fetchData]); + return ( +
+

파일 다운로드 로그

+
+ +
+ +
+ ); +} + +// ========================================== +// 메일발송 로그 (admin/log/mailSendLogList.do) +// ========================================== +function MailSendLogPage() { + const [searchTitle, setSearchTitle] = useState(""); + const [searchUser, setSearchUser] = useState(""); + const [searchType, setSearchType] = useState(""); + const [data, setData] = useState[]>([]); + const columns: GridColumn[] = [ + { title: "발송시간", field: "LOG_TIME", width: 160, hozAlign: "center" }, + { title: "시스템", field: "SYSTEM_NAME", width: 100, hozAlign: "center" }, + { title: "발신자", field: "SEND_USER_ID", width: 120 }, + { title: "수신자", field: "RECEIVER_TO", width: 200 }, + { title: "제목", field: "TITLE", width: 280 }, + { title: "유형", field: "MAIL_TYPE", width: 100, hozAlign: "center" }, + { title: "발송여부", field: "IS_SEND", width: 80, hozAlign: "center" }, + { title: "에러", field: "ERROR_LOG", width: 200 }, + ]; + const fetchData = useCallback(async () => { + const res = await fetch("/api/admin/log-mail", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ title: searchTitle, send_user_id: searchUser, mail_type: searchType }) }); + if (res.ok) { const json = await res.json(); setData(json.RESULTLIST || []); } + }, [searchTitle, searchUser, searchType]); + + useEffect(() => { fetchData(); }, [fetchData]); + return ( +
+

메일발송 로그

+ + + setSearchTitle(e.target.value)} className="w-[180px]" /> + + + setSearchUser(e.target.value)} className="w-[120px]" /> + + + setSearchType(e.target.value)} className="w-[120px]" /> + + +
+ +
+ +
+ ); +} + +// ========================================== +// 템플릿 관리 (admin/templateList.do) +// ========================================== +function TemplateManagement() { + const [searchTitle, setSearchTitle] = useState(""); + const [searchStatus, setSearchStatus] = useState("active"); + const [data, setData] = useState[]>([]); + const columns: GridColumn[] = [ + { title: "템플릿코드", field: "TEMPLATE_CODE", width: 150 }, + { title: "상세코드", field: "TEMPLATE_CODE_DETAIL", width: 150 }, + { title: "제목", field: "TITLE", width: 300 }, + { title: "등록일", field: "REG_DATE", width: 110, hozAlign: "center" }, + { title: "상태", field: "STATUS_STR", width: 80, hozAlign: "center" }, + ]; + const fetchData = useCallback(async () => { + const res = await fetch("/api/admin/template", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ title: searchTitle, status: searchStatus }) }); + if (res.ok) { const json = await res.json(); setData(json.RESULTLIST || []); } + }, [searchTitle, searchStatus]); + + useEffect(() => { fetchData(); }, [fetchData]); + return ( +
+

템플릿 관리

+ + + setSearchTitle(e.target.value)} className="w-[200px]" /> + + + + + +
+ + +
+ +
+ ); +} + +// ========================================== +// 환율관리 (admin/getExchangeRateMngList.do) +// ========================================== +function ExchangeRateManagement() { + const [searchYm, setSearchYm] = useState(""); + const [searchFrom, setSearchFrom] = useState(""); + const [searchTo, setSearchTo] = useState(""); + const [data, setData] = useState[]>([]); + const columns: GridColumn[] = [ + { title: "년월", field: "YYYY_MM", width: 90, hozAlign: "center" }, + { title: "기준일자", field: "YYYY_MM_DD", width: 100, hozAlign: "center" }, + { title: "USD", field: "COST_USD", width: 90, hozAlign: "right" }, + { title: "EUR", field: "COST_EU", width: 90, hozAlign: "right" }, + { title: "JPY", field: "COST_JAPAN", width: 90, hozAlign: "right" }, + { title: "CNY", field: "COST_CHINA", width: 90, hozAlign: "right" }, + { title: "VND", field: "COST_VIETNAM", width: 90, hozAlign: "right" }, + { title: "RUB", field: "COST_RUB", width: 90, hozAlign: "right" }, + { title: "INR", field: "COST_INR", width: 90, hozAlign: "right" }, + { title: "THB", field: "COST_THB", width: 90, hozAlign: "right" }, + { title: "HKD", field: "COST_HKD", width: 90, hozAlign: "right" }, + { title: "등록자", field: "WRITER", width: 100 }, + { title: "API수집일", field: "API_DATE", width: 140, hozAlign: "center" }, + ]; + const fetchData = useCallback(async () => { + const res = await fetch("/api/admin/exchange", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ yyyy_mm: searchYm, from_date: searchFrom, to_date: searchTo }) }); + if (res.ok) { const json = await res.json(); setData(json.RESULTLIST || []); } + }, [searchYm, searchFrom, searchTo]); + + useEffect(() => { fetchData(); }, [fetchData]); + return ( +
+

환율관리

+ + + setSearchYm(e.target.value)} placeholder="YYYYMM" className="w-[120px]" /> + + + setSearchFrom(e.target.value)} placeholder="YYYYMM" className="w-[120px]" /> + + + setSearchTo(e.target.value)} placeholder="YYYYMM" className="w-[120px]" /> + + +
+ + +
+ +
+ ); +} + +// ========================================== +// 고객사 관리 (admin/oemMngPagingList.do) +// ========================================== +function CustomerManagement() { + const [searchName, setSearchName] = useState(""); + const [searchStatus, setSearchStatus] = useState("active"); + const [data, setData] = useState[]>([]); + const columns: GridColumn[] = [ + { title: "고객사코드", field: "OEM_CODE", width: 150 }, + { title: "고객사명", field: "OEM_NAME", width: 260 }, + { title: "등록자", field: "WRITER", width: 120, hozAlign: "center" }, + { title: "등록일", field: "REGDATE", width: 110, hozAlign: "center" }, + { title: "상태", field: "STATUS_STR", width: 90, hozAlign: "center" }, + ]; + const fetchData = useCallback(async () => { + const res = await fetch("/api/admin/ref-customer", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ oem_name: searchName, status: searchStatus }) }); + if (res.ok) { const json = await res.json(); setData(json.RESULTLIST || []); } + }, [searchName, searchStatus]); + + useEffect(() => { fetchData(); }, [fetchData]); + return ( +
+

고객사 관리

+ + + setSearchName(e.target.value)} className="w-[200px]" /> + + + + + +
+ + +
+ +
+ ); +} + +// ========================================== +// 재질 관리 (admin/getMaterialMngList.do) +// ========================================== +function MaterialManagement() { + const [searchName, setSearchName] = useState(""); + const [searchStatus, setSearchStatus] = useState("active"); + const [data, setData] = useState[]>([]); + const columns: GridColumn[] = [ + { title: "재질명", field: "MATERIAL_NAME", width: 320 }, + { title: "등록자", field: "WRITER", width: 120, hozAlign: "center" }, + { title: "등록일", field: "REGDATE", width: 110, hozAlign: "center" }, + { title: "상태", field: "STATUS_STR", width: 90, hozAlign: "center" }, + ]; + const fetchData = useCallback(async () => { + const res = await fetch("/api/admin/ref-material", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ material_name: searchName, status: searchStatus }) }); + if (res.ok) { const json = await res.json(); setData(json.RESULTLIST || []); } + }, [searchName, searchStatus]); + + useEffect(() => { fetchData(); }, [fetchData]); + return ( +
+

재질 관리

+ + + setSearchName(e.target.value)} className="w-[200px]" /> + + + + + +
+ + +
+ +
+ ); +} + +// ========================================== +// 차종 관리 (admin/getCarMngList.do) +// ========================================== +function CarManagement() { + const [searchName, setSearchName] = useState(""); + const [searchCode, setSearchCode] = useState(""); + const [searchStatus, setSearchStatus] = useState("active"); + const [data, setData] = useState[]>([]); + const columns: GridColumn[] = [ + { title: "차종코드", field: "CAR_CODE", width: 120 }, + { title: "모델코드", field: "MODEL_CODE", width: 120 }, + { title: "차종명", field: "CAR_NAME", width: 180 }, + { title: "고객사", field: "OEM_NAME", width: 140 }, + { title: "설명", field: "DESCRIPTION", width: 260 }, + { title: "등록자", field: "WRITER", width: 100, hozAlign: "center" }, + { title: "등록일", field: "REGDATE", width: 110, hozAlign: "center" }, + { title: "상태", field: "STATUS_STR", width: 90, hozAlign: "center" }, + ]; + const fetchData = useCallback(async () => { + const res = await fetch("/api/admin/ref-car", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ car_name: searchName, car_code: searchCode, status: searchStatus }) }); + if (res.ok) { const json = await res.json(); setData(json.RESULTLIST || []); } + }, [searchName, searchCode, searchStatus]); + + useEffect(() => { fetchData(); }, [fetchData]); + return ( +
+

차종 관리

+ + + setSearchName(e.target.value)} className="w-[180px]" /> + + + setSearchCode(e.target.value)} className="w-[140px]" /> + + + + + +
+ + +
+ +
+ ); +} + +// ========================================== +// 제품군 관리 (admin/getProductGroupMngList.do) +// ========================================== +function ProductGroupManagement() { + const [searchName, setSearchName] = useState(""); + const [searchStatus, setSearchStatus] = useState("active"); + const [data, setData] = useState[]>([]); + const columns: GridColumn[] = [ + { title: "제품군명", field: "PRODUCT_GROUP_NAME", width: 240 }, + { title: "설명", field: "DESCRIPTION", width: 320 }, + { title: "등록자", field: "WRITER", width: 120, hozAlign: "center" }, + { title: "등록일", field: "REGDATE", width: 110, hozAlign: "center" }, + { title: "상태", field: "STATUS_STR", width: 90, hozAlign: "center" }, + ]; + const fetchData = useCallback(async () => { + const res = await fetch("/api/admin/ref-product-group", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ product_group_name: searchName, status: searchStatus }) }); + if (res.ok) { const json = await res.json(); setData(json.RESULTLIST || []); } + }, [searchName, searchStatus]); + + useEffect(() => { fetchData(); }, [fetchData]); + return ( +
+

제품군 관리

+ + + setSearchName(e.target.value)} className="w-[200px]" /> + + + + + +
+ + +
+ +
+ ); +} + +// ========================================== +// 제품 관리 (admin/getProductMngList.do) +// ========================================== +function ProductManagement() { + const [searchName, setSearchName] = useState(""); + const [searchCode, setSearchCode] = useState(""); + const [searchStatus, setSearchStatus] = useState("active"); + const [data, setData] = useState[]>([]); + const columns: GridColumn[] = [ + { title: "제품코드", field: "PRODUCT_CODE", width: 160 }, + { title: "제품명", field: "PRODUCT_NAME", width: 220 }, + { title: "제품군", field: "PRODUCT_GROUP_NAME", width: 160 }, + { title: "설명", field: "PRODUCT_DESC", width: 260 }, + { title: "등록자", field: "WRITER", width: 100, hozAlign: "center" }, + { title: "등록일", field: "REGDATE", width: 110, hozAlign: "center" }, + { title: "상태", field: "STATUS_STR", width: 90, hozAlign: "center" }, + ]; + const fetchData = useCallback(async () => { + const res = await fetch("/api/admin/ref-product", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ product_name: searchName, product_code: searchCode, status: searchStatus }) }); + if (res.ok) { const json = await res.json(); setData(json.RESULTLIST || []); } + }, [searchName, searchCode, searchStatus]); + + useEffect(() => { fetchData(); }, [fetchData]); + return ( +
+

제품 관리

+ + + setSearchName(e.target.value)} className="w-[180px]" /> + + + setSearchCode(e.target.value)} className="w-[140px]" /> + + + + + +
+ + +
+ +
+ ); +} + +// ========================================== +// 기술자료 카테고리 관리 (admin/specDataCategoryMngList.do) +// ========================================== +function SpecDataCategoryManagement() { + const [searchName, setSearchName] = useState(""); + const [searchType, setSearchType] = useState(""); + const [searchStatus, setSearchStatus] = useState("active"); + const [data, setData] = useState[]>([]); + const columns: GridColumn[] = [ + { title: "카테고리명", field: "CATEGORY_NAME", width: 200 }, + { title: "유형", field: "CATEGORY_TYPE", width: 100, hozAlign: "center" }, + { title: "사양No", field: "SPEC_NO", width: 100 }, + { title: "사양입력유형", field: "SPEC_INPUT_TYPE", width: 130, hozAlign: "center" }, + { title: "문서번호규칙", field: "DOC_NO_RULE", width: 200 }, + { title: "순서", field: "SEQ", width: 70, hozAlign: "right" }, + { title: "등록자", field: "WRITER", width: 100, hozAlign: "center" }, + { title: "등록일", field: "REGDATE", width: 110, hozAlign: "center" }, + { title: "상태", field: "STATUS_STR", width: 90, hozAlign: "center" }, + ]; + const fetchData = useCallback(async () => { + const res = await fetch("/api/admin/spec-data-category", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ category_name: searchName, category_type: searchType, status: searchStatus }) }); + if (res.ok) { const json = await res.json(); setData(json.RESULTLIST || []); } + }, [searchName, searchType, searchStatus]); + + useEffect(() => { fetchData(); }, [fetchData]); + return ( +
+

기술자료 카테고리 관리

+ + + setSearchName(e.target.value)} className="w-[180px]" /> + + + setSearchType(e.target.value)} className="w-[120px]" /> + + + + + +
+ + +
+ +
+ ); +} + +// ========================================== +// 차량옵션(사양) 관리 (admin/getCarOptionMngList.do) +// ========================================== +function CarOptionManagement() { + const [searchName, setSearchName] = useState(""); + const [searchNo, setSearchNo] = useState(""); + const [searchType, setSearchType] = useState(""); + const [searchStatus, setSearchStatus] = useState("active"); + const [data, setData] = useState[]>([]); + const columns: GridColumn[] = [ + { title: "옵션번호", field: "OPTION_NO", width: 130 }, + { title: "옵션명", field: "OPTION_NAME", width: 220 }, + { title: "영문옵션명", field: "OPTION_NAME_ENG", width: 200 }, + { title: "표준코드", field: "STD_CODE", width: 120 }, + { title: "옵션유형", field: "OPTION_TYPE", width: 100, hozAlign: "center" }, + { title: "카테고리", field: "CATEGORY", width: 120 }, + { title: "단가", field: "PRICE", width: 100, hozAlign: "right" }, + { title: "등록자", field: "WRITER", width: 100, hozAlign: "center" }, + { title: "등록일", field: "REG_DATE", width: 110, hozAlign: "center" }, + { title: "상태", field: "STATUS_STR", width: 90, hozAlign: "center" }, + ]; + const fetchData = useCallback(async () => { + const res = await fetch("/api/admin/car-option", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ option_name: searchName, option_no: searchNo, option_type: searchType, status: searchStatus }) }); + if (res.ok) { const json = await res.json(); setData(json.RESULTLIST || []); } + }, [searchName, searchNo, searchType, searchStatus]); + + useEffect(() => { fetchData(); }, [fetchData]); + return ( +
+

차량옵션(사양) 관리

+ + + setSearchName(e.target.value)} className="w-[180px]" /> + + + setSearchNo(e.target.value)} className="w-[140px]" /> + + + setSearchType(e.target.value)} className="w-[120px]" /> + + + + + +
+ + +
+ +
+ ); +} + +// ========================================== +// Placeholder +// ========================================== +function PlaceholderContent({ title }: { title: string }) { + return ( +
+
+ +

{title}

+

구현 준비 중입니다.

+
+
+ ); +} diff --git a/src/app/admin-panel/supply-form/page.tsx b/src/app/admin-panel/supply-form/page.tsx new file mode 100644 index 0000000..6f48372 --- /dev/null +++ b/src/app/admin-panel/supply-form/page.tsx @@ -0,0 +1,287 @@ +"use client"; + +import { useState, useEffect, Suspense, useCallback } from "react"; +import { useSearchParams } from "next/navigation"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { SearchableCodeSelect } from "@/components/ui/searchable-code-select"; +import Swal from "sweetalert2"; + +const CHARGER_TYPE_CODE = "0000329"; + +interface Charger { + rowKey: string; + OBJID?: string; + charger_type: string; + charger_name: string; + phone: string; + tel: string; + fax: string; + email: string; +} + +function SupplyForm() { + const searchParams = useSearchParams(); + const objid = searchParams.get("objid") || ""; + const isNew = !objid; + + const [form, setForm] = useState>({}); + const [chargers, setChargers] = useState([]); + const [loading, setLoading] = useState(false); + const set = (k: string, v: string) => setForm((p) => ({ ...p, [k]: v })); + + const newKey = () => Math.random().toString(36).slice(2, 10) + Date.now(); + + const loadDetail = useCallback(async () => { + if (!objid) return; + const res = await fetch("/api/admin/supply/detail", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ objid }), + }); + const json = await res.json(); + if (json.success && json.info) { + const info = json.info as Record; + setForm({ + objid: String(info.OBJID || ""), + supply_code: String(info.SUPPLY_CODE || ""), + supply_name: String(info.SUPPLY_NAME || ""), + area_cd: String(info.AREA_CD || ""), + charge_user_name: String(info.CHARGE_USER_NAME || ""), + bus_reg_no: String(info.BUS_REG_NO || ""), + reg_no: String(info.REG_NO || ""), + supply_busname: String(info.SUPPLY_BUSNAME || ""), + supply_stockname: String(info.SUPPLY_STOCKNAME || ""), + supply_address: String(info.SUPPLY_ADDRESS || ""), + supply_tel_no: String(info.SUPPLY_TEL_NO || ""), + email: String(info.EMAIL || ""), + office_no: String(info.OFFICE_NO || ""), + supply_fax_no: String(info.SUPPLY_FAX_NO || ""), + }); + const rows = (json.chargers || []) as Record[]; + setChargers( + rows.map((r) => ({ + rowKey: newKey(), + OBJID: String(r.OBJID || ""), + charger_type: String(r.CHARGER_TYPE || ""), + charger_name: String(r.CHARGER_NAME || ""), + phone: String(r.PHONE || ""), + tel: String(r.TEL || ""), + fax: String(r.FAX || ""), + email: String(r.EMAIL || ""), + })) + ); + } + }, [objid]); + + useEffect(() => { loadDetail(); }, [loadDetail]); + + const addCharger = () => { + setChargers((prev) => [ + ...prev, + { rowKey: newKey(), charger_type: "", charger_name: "", phone: "", tel: "", fax: "", email: "" }, + ]); + }; + + const removeCharger = (rowKey: string) => { + setChargers((prev) => prev.filter((c) => c.rowKey !== rowKey)); + }; + + const updateCharger = (rowKey: string, field: keyof Charger, value: string) => { + setChargers((prev) => prev.map((c) => (c.rowKey === rowKey ? { ...c, [field]: value } : c))); + }; + + const handleSave = async () => { + if (!form.supply_name) { + Swal.fire("알림", "고객명을 입력하세요.", "warning"); + return; + } + const r = await Swal.fire({ + title: "저장하시겠습니까?", + icon: "question", + showCancelButton: true, + confirmButtonText: "저장", + cancelButtonText: "취소", + }); + if (!r.isConfirmed) return; + setLoading(true); + try { + const res = await fetch("/api/admin/supply/save", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ ...form, objid: isNew ? "" : objid, chargers }), + }); + const data = await res.json(); + if (data.success) { + await Swal.fire({ icon: "success", title: data.message || "저장되었습니다.", timer: 1200, showConfirmButton: false }); + if (window.opener) try { window.opener.location.reload(); } catch {} + window.close(); + } else { + Swal.fire("오류", data.message || "저장 실패", "error"); + } + } catch { + Swal.fire("오류", "서버 오류", "error"); + } finally { + setLoading(false); + } + }; + + return ( +
+

+ 공급업체 등록/수정 +

+ +
+ {/* 좌: 공급업체 정보 */} +
+

공급업체 정보

+ + + + set("supply_code", v)} className="[&_input]:h-7" /> + + + set("supply_name", e.target.value)} /> + + + set("area_cd", v)} className="[&_input]:h-7" /> + + + set("charge_user_name", e.target.value)} /> + + + set("bus_reg_no", e.target.value)} /> + + + set("reg_no", e.target.value)} /> + + + set("supply_busname", e.target.value)} /> + + + set("supply_stockname", e.target.value)} /> + + +