commit 6af863199f4f7268dc6cb6a72c031aa894ec05e9 Author: chpark Date: Sat Apr 25 02:44:40 2026 +0900 feat: 모모유통 유통관리 ERP (Next.js 16) — MOMO 브랜딩 + distribution DB + momo.junggomoa.com - fito-nextjs 기반으로 재구성 - 로그인: MOMO 로고 + 모모유통 + 유통관리 ERP, 하단에 본사/지사 주소 표시 - 사이드바 상단: MOMO 아이콘 + 모모유통 + 유통관리 ERP - 파비콘: /src/app/icon.svg (MOMO 그린 배지) - layout.tsx title: 모모유통 | 유통관리 ERP - DB: 183.99.177.40:5432/distribution (fito 스키마 import 완료) - Traefik: Host(momo.junggomoa.com), 컨테이너 momo-erp 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/.gitignore b/.gitignore new file mode 100644 index 0000000..2146a06 --- /dev/null +++ b/.gitignore @@ -0,0 +1,44 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* +/logs/ + +# env files (can opt-in for committing if needed) +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts + +/src/generated/prisma diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..8bd0e39 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,5 @@ + +# 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/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..6a2f73c --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,77 @@ +# 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 new file mode 100644 index 0000000..218628d --- /dev/null +++ b/Dockerfile @@ -0,0 +1,44 @@ +# 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 + +# Stage 2: 빌드 +FROM node:20-alpine AS builder +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY . . + +# 환경변수 (빌드 타임에 주입되는 NEXT_PUBLIC_* 변수) +ENV NEXT_TELEMETRY_DISABLED=1 +ENV NODE_ENV=production + +RUN npm run build + +# Stage 3: 런타임 (최소 이미지) +FROM node:20-alpine AS runner +WORKDIR /app + +ENV NODE_ENV=production +ENV NEXT_TELEMETRY_DISABLED=1 +ENV PORT=3000 +ENV HOSTNAME=0.0.0.0 + +# 비루트 사용자 (보안) +RUN addgroup --system --gid 1001 nodejs && \ + adduser --system --uid 1001 nextjs + +# 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 + +# 파일 업로드 디렉토리 +RUN mkdir -p /data_storage && chown nextjs:nodejs /data_storage + +USER nextjs +EXPOSE 3000 + +CMD ["node", "server.js"] diff --git a/Dockerfile.dev b/Dockerfile.dev new file mode 100644 index 0000000..2ff69e3 --- /dev/null +++ b/Dockerfile.dev @@ -0,0 +1,19 @@ +# 개발환경 — hot reload 지원 +FROM node:20-alpine +WORKDIR /app + +# 의존성 캐시용 +COPY package.json package-lock.json ./ +RUN npm ci + +# 소스는 bind mount (docker-compose.dev.yml에서 지정) +COPY . . + +ENV NODE_ENV=development +ENV NEXT_TELEMETRY_DISABLED=1 +ENV PORT=3000 +ENV HOSTNAME=0.0.0.0 + +EXPOSE 3000 + +CMD ["npm", "run", "dev"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..602a220 --- /dev/null +++ b/README.md @@ -0,0 +1,70 @@ +# FITO — (주)피토 PLM (Next.js) + +기존 Java/Spring MVC + JSP + MyBatis 기반 FITO PLM을 Next.js 15 + Node.js로 컨버전한 시스템. + +- 원본: [/Users/jhj/FITO](../FITO) (Java 7 + Spring 3.2.4 + MyBatis 3.2.3 + JSP) +- DB: 외부 PostgreSQL `211.115.91.141:11140/fito` (기존 스키마 그대로 사용) +- 이전 이력: `woosung-nextjs`에서 피벗. 스냅샷 태그 `woosung-v1-snapshot`. + +## 개발 시작 + +```bash +npm install +npm run dev # http://localhost:3000 +``` + +## 환경변수 + +`.env.development`의 DB 접속 정보를 확인. 필수 키: + +- `DATABASE_URL` — 외부 PostgreSQL 접속 +- `NEXTAUTH_SECRET` — JWT 서명 키 +- `MASTER_PWD` — 마스터 비밀번호 (개발 편의용) +- `AES_KEY` — 비밀번호 AES 암호화 키 (기존 Java 호환) + +## 배포 표준 + +- Docker Compose (dev/prod 분리) — 기존 FITO(Java) 배포환경 재사용 +- Traefik 리버스 프록시 + `fito.wace.me` 도메인 (entrypoints: web, websecure / certresolver: le) +- 외부 네트워크 `toktork_server_default` +- DB는 외부 서버 공유 (`211.115.91.141:11140/fito`) — 컨테이너 내부 DB 없음 + +### `start.sh` 배포 스크립트 (권장) + +```bash +# 첫 배포 (서버에서) +cp .env.production.example .env.production +vi .env.production # DATABASE_URL, NEXTAUTH_SECRET, AES_KEY 등 입력 + +./start.sh prod # git pull → build → 기동 → Traefik 라우팅 확인 + +# 이후 배포 (git commit 후) +./start.sh prod # 자동 git pull + 재빌드 + +# 기타 운영 +./start.sh logs prod # 실시간 로그 +./start.sh restart prod # 재시작 (git pull 포함) +./start.sh stop prod # 중지 +./start.sh status prod # 컨테이너 상태 +./start.sh build prod # no-cache 재빌드 +./start.sh clean prod # 전체 삭제 (확인 필요) +``` + +스크립트는 start.sh 자체가 업데이트되면 새 버전으로 **자동 재실행**하므로 안전합니다. + +### 로컬 개발 + +```bash +./start.sh # docker 기반 (localhost:3643, hot reload) +npm run dev # docker 없이 Node 직접 (localhost:3000) +``` + +### 인프라 정보 + +- 컨테이너명: `plm-fito-next` (prod) / `plm-fito-next-dev` (dev) +- 도메인: `https://fito.wace.me` +- 내부 포트: 3000 (Traefik이 외부 80/443 → 3000) +- 파일 저장: 호스트 `./data_storage` (레포 상대경로) ↔ 컨테이너 `/data_storage` +- 이미지: Next.js `output: "standalone"` 기반 multi-stage build + +상세 구성은 [CLAUDE.md](CLAUDE.md) 참고. diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 0000000..d7f1b49 --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,24 @@ +# 로컬 개발 (hot reload) +# 사용: docker compose -f docker-compose.dev.yml up --build +services: + plm-fito-next-dev: + build: + context: . + dockerfile: Dockerfile.dev + container_name: plm-fito-next-dev + ports: + - "3643:3000" + env_file: + - .env.development + volumes: + - ./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: + plm-fito-next-data: diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 0000000..54356d0 --- /dev/null +++ b/docker-compose.prod.yml @@ -0,0 +1,25 @@ +# 운영 배포 (Traefik + momo.junggomoa.com) +# 사용: docker compose -f docker-compose.prod.yml up -d --build +services: + momo-erp: + build: + context: . + dockerfile: Dockerfile + container_name: momo-erp + restart: always + env_file: + - .env.production + volumes: + - ./data_storage:/data_storage + labels: + - traefik.enable=true + - 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=3000 + +networks: + default: + external: true + name: toktork_server_default 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..59e8341 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,8486 @@ +{ + "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", + "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", + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "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==", + "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==", + "devOptional": 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==", + "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/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==", + "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==", + "devOptional": 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==", + "dev": true, + "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..7bd87ea --- /dev/null +++ b/package.json @@ -0,0 +1,46 @@ +{ + "name": "fito-nextjs", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "eslint" + }, + "dependencies": { + "@prisma/client": "^7.7.0", + "@tanstack/react-table": "^8.21.3", + "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", + "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..12111dd --- /dev/null +++ b/public/momo-icon.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + MOMO + + + + diff --git a/public/momo-logo.svg b/public/momo-logo.svg new file mode 100644 index 0000000..72b4f2a --- /dev/null +++ b/public/momo-logo.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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/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..cb87f67 --- /dev/null +++ b/src/app/(auth)/login/page.tsx @@ -0,0 +1,142 @@ +"use client"; + +import { useState, FormEvent } from "react"; +import { useRouter } from "next/navigation"; +import Image from "next/image"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import Swal from "sweetalert2"; + +export default function LoginPage() { + const router = useRouter(); + const [userId, setUserId] = useState(""); + const [password, setPassword] = useState(""); + 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("/dashboard"); + } else { + Swal.fire({ + icon: "error", + title: "로그인 실패", + text: data.message || "아이디 또는 비밀번호를 확인하세요.", + }); + } + } catch { + Swal.fire({ icon: "error", title: "서버 오류", text: "잠시 후 다시 시도하세요." }); + } finally { + setLoading(false); + } + }; + + return ( +
+
+
+
+ MOMO +

+ 모모유통 +

+

+ 유통관리 ERP +

+
+ +
+
+ + setUserId(e.target.value)} + placeholder="사용자 아이디를 입력하세요" + autoFocus + autoComplete="username" + /> +
+ +
+ + setPassword(e.target.value)} + placeholder="비밀번호를 입력하세요" + autoComplete="current-password" + /> +
+ + +
+ +

+ © 2026 MOMO DISTRIBUTION. All rights reserved. +

+
+
+ +
+
+
+ MOMO + + 모모유통 + +
+
+

+ 본사 + 경기도 의왕시 벌모루길 46 B동 +

+

+ 지사 + 경기도 김포시 고촌읍 김포대로 451번길 210 +

+

+ T 010-6624-5315  ·  E momo8443@daum.net +

+
+
+
+
+ ); +} 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)} /> + + +