diff --git a/IMK_상품_업로드.csv b/IMK_상품_업로드.csv new file mode 100644 index 0000000..1de033b --- /dev/null +++ b/IMK_상품_업로드.csv @@ -0,0 +1,16 @@ +상품명,상품코드,모델번호,카테고리,규격,제조사,공급사,원산지,상품 단가,최저한도,매입가,판매가,이미지URL,최소주문수량,리드타임(일),단위,배송형태,부가세포함(Y/N),배송비포함(Y/N) +27형 QHD 모니터,IMK-MN-2701,VX2718-Q,사무기기,27인치 / QHD 2560x1440,비전텍,글로벌테크놀로지,한국,298000,279000,265000,329000,https://images.unsplash.com/photo-1527443224154-c4a3942d3acf?w=640&q=80,1,7,EA,협력사배송,N,Y +27형 올인원 PC,IMK-PC-2810,AIO-2740,사무기기,27인치 / i5 / 16GB / 512GB,비전텍,글로벌테크놀로지,한국,1190000,1120000,1050000,1290000,https://images.unsplash.com/photo-1547082299-de196ea013d6?w=640&q=80,1,10,EA,협력사배송,N,Y +무선 슬림 키보드,IMK-KB-3312,SK-110W,사무기기,텐키리스 / 2.4GHz,커넥타,글로벌테크놀로지,중국,32000,28500,24900,39000,https://images.unsplash.com/photo-1587829741301-dc798b83add3?w=640&q=80,10,3,EA,지정택배배송,Y,Y +무선 무소음 마우스,IMK-MS-3305,WM-540G,사무기기,2.4GHz / 무소음 클릭,커넥타,글로벌테크놀로지,중국,18500,16900,14800,22000,https://images.unsplash.com/photo-1527864550417-7fd91fc51a46?w=640&q=80,10,3,EA,지정택배배송,Y,Y +노이즈캔슬링 헤드셋,IMK-HS-3520,NC-800,사무기기,블루투스 5.3 / ANC,사운드코어,글로벌테크놀로지,중국,128000,115000,102000,149000,https://images.unsplash.com/photo-1608043152269-423dbba4e7e1?w=640&q=80,5,5,EA,지정택배배송,Y,Y +블루투스 회의용 스피커,IMK-SP-3618,BS-330,사무기기,20W / IPX5,사운드코어,글로벌테크놀로지,중국,89000,79000,69500,105000,https://images.unsplash.com/photo-1585771724684-38269d6639fd?w=640&q=80,5,5,EA,지정택배배송,Y,Y +인스턴트 카메라,IMK-CM-3901,IC-500W,사무기기,즉석인화 / 홍보·행사용,포토랩,글로벌테크놀로지,일본,145000,132000,118000,169000,https://images.unsplash.com/photo-1526170375885-4d8ecf77b99f?w=640&q=80,2,7,EA,지정택배배송,Y,N +패브릭 라운지 체어,IMK-CH-1102,LC-220BE,사무가구,W600 x D640 x H780,퍼니코,대한정밀,한국,187000,172000,159000,209000,https://images.unsplash.com/photo-1580480055273-228ff5388ef8?w=640&q=80,2,14,EA,협력사배송,N,N +LED 데스크 스탠드,IMK-LT-1230,DL-410G,사무가구,3단 밝기 / 각도조절,루멘랩,대한정밀,한국,46000,41000,36500,55000,https://images.unsplash.com/photo-1507473885765-e6ed057f782c?w=640&q=80,5,7,EA,협력사배송,Y,Y +데스크 미니 화분,IMK-PL-1305,GP-100M,사무가구,다육 / 지름 10cm 화기 포함,그린데스크,대한정밀,한국,12500,10900,9200,15000,https://images.unsplash.com/photo-1485955900006-10f4d324d411?w=640&q=80,10,5,EA,협력사배송,Y,N +스페셜티 원두 1kg,IMK-CF-7701,RB-1000,탕비용품,아라비카 100% / 홀빈,빈스터,서울산업소재,한국,29800,26500,23000,35000,https://images.unsplash.com/photo-1511920170033-f8396924c348?w=640&q=80,5,4,BAG,지정택배배송,Y,Y +스테인리스 텀블러 500ml,IMK-TB-7802,ST-500KH,탕비용품,진공 2중 / 500ml,웜쿨,서울산업소재,한국,16800,14900,12800,21000,https://images.unsplash.com/photo-1602143407151-7111542de6e8?w=640&q=80,20,5,EA,지정택배배송,Y,Y +핸드메이드 솝바 세트,IMK-SB-7910,HS-3SET,생활용품,천연비누 3입,클린어스,서울산업소재,한국,13500,11900,9800,16500,https://images.unsplash.com/photo-1584305574647-0cc949a2bb9f?w=640&q=80,10,5,SET,지정택배배송,Y,Y +스프링 노트 A5 10권,IMK-NT-8011,SN-A5-10,사무용품,A5 / 80매 / 10권 팩,페이퍼웍스,서울산업소재,한국,21000,18500,15900,25000,https://images.unsplash.com/photo-1531346878377-a5be20888e57?w=640&q=80,5,3,PACK,픽업배송,Y,Y +젤 잉크 볼펜 12자루,IMK-PN-8105,GP-05BK,사무용품,0.5mm / 흑색 / 12입,페이퍼웍스,서울산업소재,한국,8400,7200,5900,10500,https://images.unsplash.com/photo-1583485088034-697b5bc54ccd?w=640&q=80,10,3,BOX,픽업배송,Y,Y diff --git a/IMK_협력사_업로드.csv b/IMK_협력사_업로드.csv new file mode 100644 index 0000000..e304e62 --- /dev/null +++ b/IMK_협력사_업로드.csv @@ -0,0 +1,4 @@ +협력사명,식별코드,담당자명,담당자이메일,총매출액,취급상품 +글로벌테크놀로지,global,김지훈 팀장,jh.kim@globaltech.co.kr,12000000000,"27형 QHD 모니터, 27형 올인원 PC, 무선 슬림 키보드, 무선 무소음 마우스, 노이즈캔슬링 헤드셋, 블루투스 회의용 스피커, 인스턴트 카메라" +대한정밀,daehan,박서연 과장,sy.park@daehanpm.co.kr,8500000000,"패브릭 라운지 체어, LED 데스크 스탠드, 데스크 미니 화분" +서울산업소재,seoul,이민호 대리,mh.lee@seoulmat.co.kr,5200000000,"스페셜티 원두 1kg, 스테인리스 텀블러 500ml, 핸드메이드 솝바 세트, 스프링 노트 A5 10권, 젤 잉크 볼펜 12자루" diff --git a/docker-compose.prod.landing.yml b/docker-compose.prod.landing.yml new file mode 100644 index 0000000..fe0e62e --- /dev/null +++ b/docker-compose.prod.landing.yml @@ -0,0 +1,214 @@ +services: + # ── 프론트 (React/Vite → 정적 빌드 → nginx) ────────────────────────────── + negodata-front: + build: + context: ./negodata/front + dockerfile: Dockerfile.prod + container_name: negodata-front + ports: + - "${NEGODATA_FRONT_PORT:-30012}:80" + depends_on: + - negodata-backend # nginx 가 시작 시 upstream(negodata-backend) 이름을 해석해야 함 + restart: unless-stopped + + negosium-front: + build: + context: ./frontend + dockerfile: Dockerfile.prod + container_name: negosium-front + ports: + - "${NEGOSIUM_FRONT_PORT:-30010}:80" + depends_on: + - negosium-backend + restart: unless-stopped + + # ── 솔루션 랜딩 (React Router SSG → 정적 빌드 → nginx) ─────────────────── + landing: + build: + context: ./landing + dockerfile: Dockerfile.prod + container_name: negosium-landing + ports: + - "${LANDING_PORT:-30013}:80" + restart: unless-stopped + + # ── 백엔드 (FastAPI, 기존 Dockerfile) ──────────────────────────────────── + negodata-backend: + build: ./negodata/backend + container_name: negodata-backend + environment: + APP_ENV: prod + SCHEDULER_ENABLED: "1" # 견적 자동마감 크론(process_count=1 단일 워커라 중복 없음) + PYTHONUNBUFFERED: "1" + LPS_BASE_URL: http://lps-api:9600 # 최저가 검색요청 enqueue — 같은 compose 망의 lps-api 컨테이너(미설정 시 localhost:9600 → 연결실패) + # LPS 읽기 DB(lps_db)는 config.prod.toml 의 [LpsDBConfig](172.30.1.36/o2o_db_admin) 이 담당 → 별도 env 불필요 + DB_HOST: ${DB_HOST} # toml 의 127.0.0.1 을 외부 DB 로 override + DB_PORT: ${DB_PORT:-5432} + DB_USER: ${DB_USER} + DB_PASSWORD: ${DB_PASSWORD} + DB_NAME: ${DB_NAME:-negosium_db} + volumes: + - ./negodata/backend/config/config.prod.toml:/app/config/config.prod.toml:ro + # 외부 비노출 — negodata-front 의 nginx 가 /v1 을 내부망으로 프록시. + extra_hosts: + - "host.docker.internal:host-gateway" # DB 가 이 서버 호스트면 DB_HOST=host.docker.internal + restart: unless-stopped + + negosium-backend: + build: ./backend + container_name: negosium-backend + environment: + APP_ENV: prod + AGENT_BASE_URL: http://agent:9500 # 내부망으로 agent 호출 + PYTHONUNBUFFERED: "1" + DB_HOST: ${DB_HOST} + DB_PORT: ${DB_PORT:-5432} + DB_USER: ${DB_USER} + DB_PASSWORD: ${DB_PASSWORD} + DB_NAME: ${DB_NAME:-negosium_db} + volumes: + - ./backend/config/config.prod.toml:/app/config/config.prod.toml:ro + # 외부 비노출 — negosium-front 의 nginx 가 /v1 을 내부망으로 프록시. + extra_hosts: + - "host.docker.internal:host-gateway" + restart: unless-stopped + + # ── 협상 에이전트 (Python, 내부 전용) ──────────────────────────────────── + agent: + build: ./agent + container_name: negosium-agent + environment: + APP_ENV: prod + PYTHONUNBUFFERED: "1" + DB_HOST: ${DB_HOST} + DB_PORT: ${DB_PORT:-5432} + DB_USER: ${DB_USER} + DB_PASSWORD: ${DB_PASSWORD} + DB_NAME: ${DB_NAME:-negosium_db} + volumes: + - ./agent/config/config.prod.toml:/app/config/config.prod.toml:ro + extra_hosts: + - "host.docker.internal:host-gateway" + restart: unless-stopped + + # ── 앵커링 값 자동 조정 배치 (negosium_db 공유, 포트 없음 — 상주 스케줄러) ── + # 이미지는 config.{APP_ENV}.toml 을 읽는다(dev/prod 는 파일 없으면 기동 중단). DB/시크릿은 + # config.prod.toml(172.30.1.36) 에서 온다 — env(DB_*/REDIS_*) 를 설정하면 파일값을 override. + anchoring: + build: ./schedules/anchoring + container_name: anchoring + environment: + APP_ENV: prod # → config.prod.toml 선택 (local 로 두면 빈 config.local 로 기동 실패) + REDIS_HOST: anchoring-redis # 전용 캐시 컨테이너 사용(config 의 redis host 를 override) + TZ: Asia/Seoul + volumes: + - ./schedules/anchoring/config.prod.toml:/app/config.prod.toml:ro # DB 접속·시크릿은 파일에서(마운트 필수) + depends_on: + - anchoring-redis + extra_hosts: + - "host.docker.internal:host-gateway" + restart: unless-stopped + logging: # 상주 배치 — 장기 운영 디스크 보호 + driver: json-file + options: + max-size: "10m" + max-file: "5" + + # 앵커링 조회 캐시 (anchoring 전용) + anchoring-redis: + image: redis:7-alpine + container_name: anchoring-redis + ports: + - "127.0.0.1:6380:6379" # 호스트 로컬만 개방 (무인증 Redis). 6380 = 호스트 redis(6379)와 충돌 회피 + restart: unless-stopped + logging: + driver: json-file + options: + max-size: "10m" + max-file: "5" + + # ── LPS (인터넷 최저가 검색) ────────────────────────────────── + # API(요청 접수) + 워커(크롤, Chromium+Xvfb). + # 이미지는 APP_ENV=local 고정 → config.local.toml 을 읽는다. prod 실값(DB 172.30.1.36 / + # o2o_db_admin / negosium_db + OpenAI·Naver·Decodo 시크릿)은 config.prod.toml 을 그 경로에 + # 마운트해 주입한다(다른 prod 서비스와 동일한 파일 기반 방식). DB_* env 는 주지 않는다 — + # 주면 파일값을 빈 값/오타로 덮어써 인증 실패(예: 사용자 o2oadmin)한다. + lps-api: + build: + context: ./lps + dockerfile: Dockerfile + container_name: lps-api + environment: + APP_ENV: local # 이미지 고정값 — 변경 금지(config.prod.toml 을 직접 읽지 않음) + PYTHONUNBUFFERED: "1" + PROCESS_COUNT: ${LPS_API_PROCESS_COUNT:-1} + DB_CONNECTION_BUDGET: ${LPS_DB_CONNECTION_BUDGET:-40} # 전용 PG(max_connections≈100)면 90 근처로 상향 + volumes: + - ./lps/config/config.prod.toml:/app/config/config.local.toml:ro # prod DB·시크릿 주입(APP_ENV=local 이 읽는 경로 덮어씀) + ports: + - "9600:9600" + extra_hosts: + - "host.docker.internal:host-gateway" + labels: + autoheal: "true" # HEALTHCHECK 실패 시 autoheal 이 재시작 + restart: unless-stopped + logging: + driver: json-file + options: { max-size: "10m", max-file: "5" } + + lps-worker: + build: + context: ./lps + dockerfile: Dockerfile.worker # Chromium + Xvfb (headless 는 안티봇에 탐지됨) + container_name: lps-worker + environment: + APP_ENV: local # 이미지 고정값 — 변경 금지 + PYTHONUNBUFFERED: "1" + WORKER_CONCURRENCY: "1" # 상품 동시 검색 수(워커별 브라우저 세트, Chrome 4×N) + LPS_PROFILE_DIR: /profiles # Chrome 프로필을 영속 볼륨에 → 재시작해도 cf_clearance 유지 + # DB·시크릿(OpenAI/Naver/Decodo)은 아래 config.prod.toml 마운트에서 온다. env override 는 주지 않는다. + volumes: + - ./lps/config/config.prod.toml:/app/config/config.local.toml:ro # prod DB·시크릿 주입(APP_ENV=local 이 읽는 경로 덮어씀) + - lps-profiles:/profiles # Chrome 프로필(쿠키) 영속 + extra_hosts: + - "host.docker.internal:host-gateway" + shm_size: "1gb" # Chrome 는 /dev/shm 을 많이 씀 — 부족하면 탭 크래시 + stop_grace_period: 75s # graceful 종료 유예 — 기본 10s 면 하던 잡 마무리 전에 SIGKILL + labels: + autoheal: "true" # 하트비트 HEALTHCHECK 실패(행/좀비) 시 autoheal 이 재시작 + restart: unless-stopped + logging: + driver: json-file + options: { max-size: "10m", max-file: "5" } + + # LPS 관리자 UI (정적 React → nginx). /v1·/healthz·/readyz 는 nginx 가 lps-api:9600 으로 프록시(동일 compose 망). + lps-admin: + build: + context: ./lps-admin + dockerfile: Dockerfile + container_name: lps-admin + ports: + - "${LPS_ADMIN_PORT:-30014}:80" + depends_on: + - lps-api + restart: unless-stopped + logging: + driver: json-file + options: { max-size: "10m", max-file: "5" } + + # HEALTHCHECK 실패 컨테이너 자동 재시작 — autoheal 라벨 붙은 컨테이너(lps-api/lps-worker)를 감시해 재시작. + # docker.sock 마운트 = 도커 제어 권한이므로 신뢰 환경에서만 사용. + autoheal: + image: willfarrell/autoheal:latest + container_name: autoheal + environment: + AUTOHEAL_CONTAINER_LABEL: autoheal + volumes: + - /var/run/docker.sock:/var/run/docker.sock + restart: unless-stopped + logging: + driver: json-file + options: { max-size: "10m", max-file: "5" } + +volumes: + lps-profiles: diff --git a/logs.sh b/logs.sh index e8dabc5..6a999ef 100755 --- a/logs.sh +++ b/logs.sh @@ -1,18 +1,69 @@ #!/usr/bin/env bash -# 운영 로그 보기 — docker-compose.prod.yml 기준. 서비스명은 compose 의 아무 서비스나. -# ./logs.sh → 전체 서비스 실시간 -# ./logs.sh negodata-backend → 그 서비스만 실시간 -# ./logs.sh negosium-backend 200 → 마지막 200줄만 (실시간 아님) +# 운영 로그 보기 — 실행하면 실행중인 컨테이너 번호 메뉴, 0=전체 +# ./logs.sh → 메뉴에서 선택 +# ./logs.sh 3 → 메뉴 안 띄우고 3번 바로 +# ./logs.sh lps-api → 이름으로 바로 set -e -cd "$(dirname "$0")" -SVC="$1" -LINES="$2" +TAIL=100 -if [ -z "$SVC" ]; then - docker compose -f docker-compose.prod.yml logs -f --tail 100 -elif [ -z "$LINES" ]; then - docker compose -f docker-compose.prod.yml logs -f --tail 100 "$SVC" -else - docker compose -f docker-compose.prod.yml logs --tail "$LINES" "$SVC" +# 우리 프로젝트 컨테이너만 (negosium-db는 compose 라벨이 없어 이름으로 포함) +containers=() +while IFS= read -r name; do + containers+=("$name") +done < <( + { + docker ps --filter label=com.docker.compose.project=o2o-negosium --format '{{.Names}}' + docker ps --filter name='^negosium-db$' --format '{{.Names}}' + } | sort -u +) + +if [ ${#containers[@]} -eq 0 ]; then + echo "실행 중인 컨테이너가 없습니다." + exit 1 +fi + +follow_all() { + # docker logs -f 는 컨테이너 하나씩만 되므로 백그라운드로 전부 띄우고 prefix 붙임 + trap 'kill $(jobs -p) 2>/dev/null; exit 0' INT TERM + local colors=(31 32 33 34 35 36 91 92 93 94 95 96) + local i=0 + for c in "${containers[@]}"; do + local col=${colors[$((i % ${#colors[@]}))]} + docker logs -f --tail "$TAIL" "$c" 2>&1 | sed -u "s/^/$(printf '\033[%sm[%s]\033[0m ' "$col" "$c")/" & + i=$((i + 1)) + done + wait +} + +follow_one() { + echo "=== $1 (tail $TAIL, Ctrl+C 종료) ===" + exec docker logs -f --tail "$TAIL" "$1" +} + +pick="$1" + +if [ -z "$pick" ]; then + echo " 0) 전체 (모든 컨테이너)" + for idx in "${!containers[@]}"; do + printf "%2d) %s\n" "$((idx + 1))" "${containers[$idx]}" + done + printf "번호 선택: " + read -r pick + if [ -z "$pick" ]; then + exit 0 + fi +fi + +if [ "$pick" = "0" ]; then + follow_all +elif [[ "$pick" =~ ^[0-9]+$ ]]; then + sel=$((pick - 1)) + if [ "$sel" -lt 0 ] || [ "$sel" -ge ${#containers[@]} ]; then + echo "잘못된 번호: $pick" + exit 1 + fi + follow_one "${containers[$sel]}" +else + follow_one "$pick" fi diff --git a/negodata/front/eslint.config.js b/negodata/front/eslint.config.js new file mode 100644 index 0000000..398c0ba --- /dev/null +++ b/negodata/front/eslint.config.js @@ -0,0 +1,32 @@ +// 최소 게이트 — 전체 스타일 린트가 아니라, tsc 가 못 잡는 런타임 크래시 버그만 막는다. +// 도입 계기: QuotationDetailSheet 에서 로컬 const canManage 가 동명 import 를 가려 +// zustand 셀렉터가 TDZ 참조 → 프로덕션 크래시(Cannot access 'k' before initialization). +// 그 케이스는 순환 타입 추론으로 tsc --noEmit 도 통과했다. +import tseslint from 'typescript-eslint'; +import reactHooks from 'eslint-plugin-react-hooks'; + +export default tseslint.config({ + files: ['src/**/*.{ts,tsx}'], + ignores: ['src/api/generated/**'], + languageOptions: { + parser: tseslint.parser, + }, + plugins: { + '@typescript-eslint': tseslint.plugin, + 'react-hooks': reactHooks, + }, + rules: { + // 훅 호출 순서 위반은 런타임 크래시라 error, deps 누락은 기존 코드가 많아 warn. + 'react-hooks/rules-of-hooks': 'error', + 'react-hooks/exhaustive-deps': 'warn', + // 선언보다 위에서 변수를 쓰는 것(TDZ) 차단. 함수/타입 호이스팅은 안전하므로 허용. + 'no-use-before-define': 'off', + '@typescript-eslint/no-use-before-define': [ + 'error', + { functions: false, classes: false, typedefs: false, enums: false, variables: true }, + ], + // 상위 스코프(import 포함) 이름을 가리는 재선언 차단. + 'no-shadow': 'off', + '@typescript-eslint/no-shadow': 'error', + }, +}); diff --git a/negodata/front/package-lock.json b/negodata/front/package-lock.json index a7d25ad..a999bef 100644 --- a/negodata/front/package-lock.json +++ b/negodata/front/package-lock.json @@ -46,10 +46,13 @@ "@types/react-dom": "^19.2.3", "autoprefixer": "^10.4.21", "esbuild": "^0.25.0", + "eslint": "^10.7.0", + "eslint-plugin-react-hooks": "^7.1.1", "orval": "^7.3.0", "tailwindcss": "^4.1.14", "tsx": "^4.21.0", "typescript": "~5.8.2", + "typescript-eslint": "^8.65.0", "vite": "^6.2.3" } }, @@ -1191,6 +1194,113 @@ "node": ">=18" } }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", + "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.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.6.0.tgz", + "integrity": "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.2.tgz", + "integrity": "sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, "node_modules/@exodus/schemasafe": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@exodus/schemasafe/-/schemasafe-1.3.0.tgz", @@ -1307,6 +1417,72 @@ "react-hook-form": "^7.55.0" } }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.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/@ibm-cloud/openapi-ruleset": { "version": "1.33.10", "resolved": "https://registry.npmjs.org/@ibm-cloud/openapi-ruleset/-/openapi-ruleset-1.33.10.tgz", @@ -3755,6 +3931,13 @@ "@types/node": "*" } }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", @@ -3926,6 +4109,249 @@ "integrity": "sha512-lrpDziQipxCEeK5kWxvljWYhUvOiB2A9izZd9B2AFarYAkqZshb4lPbRs7zKEic6eGtH8V/2qJW+dPp9OtF6bw==", "license": "MIT" }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.65.0.tgz", + "integrity": "sha512-IEgob78X12rHpUmtcwFsXhZdVGJtwTVP8FiCLZkR6GlYVrl2PcuB+KhCE5BlVC/eQpQnu8WXRtkHZuPar+gCRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.65.0", + "@typescript-eslint/type-utils": "8.65.0", + "@typescript-eslint/utils": "8.65.0", + "@typescript-eslint/visitor-keys": "8.65.0", + "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.65.0", + "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.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.6.tgz", + "integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.65.0.tgz", + "integrity": "sha512-CZ4nMxWwgu1HEEFNkeaCptra9QCtkmKdgf3sWh1rl1trIhmxLilgTV4cwcbQ4wemnT4sWQN8CaKOmdYx+g2gMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.65.0", + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/typescript-estree": "8.65.0", + "@typescript-eslint/visitor-keys": "8.65.0", + "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.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.65.0.tgz", + "integrity": "sha512-SxnPhbTsGahizDgbu7oqFH/xVtzIqMd/s+WtnSxNxJZJpLbdT5IPdzg8EZxO3+PoKahXmwJLeNQOpKJb3/bi7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.65.0", + "@typescript-eslint/types": "^8.65.0", + "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.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.65.0.tgz", + "integrity": "sha512-Esbl8OSYiVxBokYgWPf7VVWg/BE798wXhimnn9ML9Pt5qoDf8bfQlgjlKXR/k98+AcNzlLKYrpCcrcuZ9DZLgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/visitor-keys": "8.65.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/tsconfig-utils": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.65.0.tgz", + "integrity": "sha512-j6GzGqCiRdA7Qhur2VVmKZAkBLfnHFQfx4TaJGL9RMveZqCo48jSHHO0DTgizEnGhtWnqmbtCUSrqSkdiY/0Hg==", + "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.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.65.0.tgz", + "integrity": "sha512-YjaZ7PRI5qY7ax2L3PbvX0rRyGtipAReCWs0mhhDBHjH/vl0g0BonaGXrKdKpMbIIsMIwDgbk/xzkBTyAltS5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/typescript-estree": "8.65.0", + "@typescript-eslint/utils": "8.65.0", + "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.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.65.0.tgz", + "integrity": "sha512-JSSwWNy+H0E/01jJEM+hrX6N0OFDzFzeIhHFSAS01tlVaevpG8cFyYRPhS5yjGOvBUx3sqQHVMjCL1CAZZMxBg==", + "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.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.65.0.tgz", + "integrity": "sha512-JboAE2swaYt4tb1fHhHTABE2K+OLy09XfcTbhnk4Pw96f9dd2e9iYsJ28gBggHlo5z5x1rkyWvcPoTuNTd4oGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.65.0", + "@typescript-eslint/tsconfig-utils": "8.65.0", + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/visitor-keys": "8.65.0", + "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/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.65.0.tgz", + "integrity": "sha512-gXiwIHsYreboxeJucHKPvgwl7dXt50mF8s1/c00cP/WoVTyWKFdtfhRWwZiXYFU5H2O8vVoSLNrexFZjYS/SGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.65.0", + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/typescript-estree": "8.65.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/visitor-keys": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.65.0.tgz", + "integrity": "sha512-8C71BQkGjiMmXtop7pHVJu1l2NNShFdkCyD6a2ezzs5vU/L3LRtb69EtcteFwz0mYMPzIgOw0n6OV4VBUWZd7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.65.0", + "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/@vitejs/plugin-react": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.2.0.tgz", @@ -3985,6 +4411,16 @@ "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/agent-base": { "version": "7.1.4", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", @@ -5032,6 +5468,13 @@ } } }, + "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": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", @@ -5561,6 +6004,185 @@ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", "license": "MIT" }, + "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": "10.7.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.7.0.tgz", + "integrity": "sha512-GVTD7s1vdIl6UYvAfriOPeY1Df8LIZjfofLvHwde+erDHGGuHyuM6xoxRxmHiebhYuD2p1vN4wWh0XzPARSGDQ==", + "dev": true, + "license": "MIT", + "workspaces": [ + "packages/*" + ], + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.6.0", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.2", + "@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", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.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", + "minimatch": "^10.2.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz", + "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==", + "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 || ^10.0.0" + } + }, + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "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/eslint/node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "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/eslint/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/eslint/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/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/esprima": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", @@ -5574,6 +6196,42 @@ "node": ">=4" } }, + "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", @@ -5773,6 +6431,20 @@ "node": ">=8.6.0" } }, + "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-memoize": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/fast-memoize/-/fast-memoize-2.5.2.tgz", @@ -5867,6 +6539,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "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/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -5929,6 +6614,27 @@ "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.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.3.tgz", + "integrity": "sha512-/zipXxyO6rGvuNGDiULY9MvEGSkb2gaG4GGH4ygMi0ZZzyMHdUZBmntJmx5x1G2VuPytCwGN4xsJP6cw+sK+vQ==", + "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", @@ -6440,6 +7146,23 @@ "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.25", "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.25.tgz", @@ -6546,6 +7269,16 @@ "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/inflected": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/inflected/-/inflected-2.1.0.tgz", @@ -7252,6 +7985,13 @@ "bignumber.js": "^9.0.0" } }, + "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-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", @@ -7270,6 +8010,13 @@ "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", "license": "BSD-2-Clause" }, + "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", @@ -7361,6 +8108,16 @@ "safe-buffer": "^5.0.1" } }, + "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/kleur": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", @@ -7380,6 +8137,20 @@ "node": ">=6" } }, + "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", @@ -8062,6 +8833,13 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "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/negotiator": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", @@ -8429,6 +9207,24 @@ "yaml": "^2.8.0" } }, + "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/ora": { "version": "8.2.0", "resolved": "https://registry.npmjs.org/ora/-/ora-8.2.0.tgz", @@ -8920,6 +9716,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "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/pretty-ms": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.3.0.tgz", @@ -8994,6 +9800,16 @@ "node": ">= 0.10" } }, + "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/punycode.js": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", @@ -10489,6 +11305,19 @@ "dev": true, "license": "MIT" }, + "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/ts-morph": { "version": "26.0.0", "resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-26.0.0.tgz", @@ -11026,6 +11855,19 @@ "url": "https://github.com/sponsors/Wombosvideo" } }, + "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/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -11181,6 +12023,30 @@ "node": ">=14.17" } }, + "node_modules/typescript-eslint": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.65.0.tgz", + "integrity": "sha512-/ggrHAwyjENDusvyxbuqxAC2dTnZg/Z8F+fgQtYIz+L6n/9HfSlEZcFGV/NsMNa6CkGk0xUjUAFwC0vHOflvIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.65.0", + "@typescript-eslint/parser": "8.65.0", + "@typescript-eslint/typescript-estree": "8.65.0", + "@typescript-eslint/utils": "8.65.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/uc.micro": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", @@ -11273,6 +12139,16 @@ "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/urijs": { "version": "1.19.11", "resolved": "https://registry.npmjs.org/urijs/-/urijs-1.19.11.tgz", @@ -11625,6 +12501,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "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/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -11849,6 +12735,19 @@ "zod": "^3.25.28 || ^4" } }, + "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.14", "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.14.tgz", diff --git a/negodata/front/package.json b/negodata/front/package.json index 6a2c03a..c0d21f9 100644 --- a/negodata/front/package.json +++ b/negodata/front/package.json @@ -5,10 +5,10 @@ "type": "module", "scripts": { "dev": "vite --port=3000 --host=0.0.0.0", - "build": "vite build", + "build": "tsc --noEmit && eslint src && vite build", "preview": "vite preview", "clean": "rm -rf dist server.js", - "lint": "tsc --noEmit", + "lint": "tsc --noEmit && eslint src", "orval": "orval --config ./orval.config.ts" }, "dependencies": { @@ -50,10 +50,13 @@ "@types/react-dom": "^19.2.3", "autoprefixer": "^10.4.21", "esbuild": "^0.25.0", + "eslint": "^10.7.0", + "eslint-plugin-react-hooks": "^7.1.1", "orval": "^7.3.0", "tailwindcss": "^4.1.14", "tsx": "^4.21.0", "typescript": "~5.8.2", + "typescript-eslint": "^8.65.0", "vite": "^6.2.3" } } diff --git a/negodata/front/src/components/ui/chart.tsx b/negodata/front/src/components/ui/chart.tsx index c2351e3..5749df1 100644 --- a/negodata/front/src/components/ui/chart.tsx +++ b/negodata/front/src/components/ui/chart.tsx @@ -60,7 +60,7 @@ function ChartContainer({ ); } -const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => { +function ChartStyle({ id, config }: { id: string; config: ChartConfig }) { const colorConfig = Object.entries(config).filter(([, c]) => c.theme || c.color); if (!colorConfig.length) return null; @@ -85,11 +85,11 @@ ${colorConfig }} /> ); -}; +} const ChartTooltip = RechartsPrimitive.Tooltip; -// eslint-disable-next-line @typescript-eslint/no-explicit-any + type TooltipContentProps = { active?: boolean; payload?: any[]; @@ -214,7 +214,7 @@ const ChartLegend = RechartsPrimitive.Legend; type LegendContentProps = { className?: string; hideIcon?: boolean; - // eslint-disable-next-line @typescript-eslint/no-explicit-any + payload?: any[]; verticalAlign?: 'top' | 'bottom' | 'middle'; nameKey?: string; @@ -251,7 +251,7 @@ function ChartLegendContent({ } // payload 항목에서 config 매칭 키를 찾는다(shadcn 원본 로직). -// eslint-disable-next-line @typescript-eslint/no-explicit-any + function getPayloadConfigFromPayload(config: ChartConfig, payload: any, key: string) { if (typeof payload !== 'object' || payload === null) return undefined; diff --git a/negodata/front/src/components/ui/phone-input.tsx b/negodata/front/src/components/ui/phone-input.tsx index 2d77209..b2651ee 100644 --- a/negodata/front/src/components/ui/phone-input.tsx +++ b/negodata/front/src/components/ui/phone-input.tsx @@ -9,7 +9,7 @@ type PhoneInputProps = Omit, 'value' | 'onCha onChange?: (value: string) => void; // 숫자만으로 방출 }; -export const PhoneInput = forwardRef(function PhoneInput( +export const PhoneInput = forwardRef(function PhoneInputInner( { value = '', onChange, ...props }, ref, ) { diff --git a/negodata/front/src/features/cards/editor/CardScriptToolbar.tsx b/negodata/front/src/features/cards/editor/CardScriptToolbar.tsx index d5a60c5..f0cd74d 100644 --- a/negodata/front/src/features/cards/editor/CardScriptToolbar.tsx +++ b/negodata/front/src/features/cards/editor/CardScriptToolbar.tsx @@ -123,4 +123,6 @@ function MarkButton({ ); } -const Divider = () => ; +function Divider() { + return ; +} diff --git a/negodata/front/src/features/quotations/components/QuotationCreateModal.tsx b/negodata/front/src/features/quotations/components/QuotationCreateModal.tsx index 81c1694..425c510 100644 --- a/negodata/front/src/features/quotations/components/QuotationCreateModal.tsx +++ b/negodata/front/src/features/quotations/components/QuotationCreateModal.tsx @@ -384,13 +384,13 @@ export function QuotationCreateModal({ {/* Steps indicator — 스텝 수는 유형에 따라 3(경매)/4(협상) */}
- {steps.map((label, i) => { + {steps.map((stepLabel, i) => { const n = i + 1; return ( -
+
{i > 0 && }
); diff --git a/negodata/front/src/features/quotations/components/QuotationDetailSheet/index.tsx b/negodata/front/src/features/quotations/components/QuotationDetailSheet/index.tsx index cbfad3a..1d34300 100644 --- a/negodata/front/src/features/quotations/components/QuotationDetailSheet/index.tsx +++ b/negodata/front/src/features/quotations/components/QuotationDetailSheet/index.tsx @@ -78,10 +78,10 @@ export function QuotationDetailSheet({ // 프론트 1차 차단이며, 실제 보안은 백엔드가 동일 스코프로 강제해야 함(버튼 숨김만으론 우회 가능). const myUserId = useAuthStore((s) => s.user?.userId); const isSuperAdmin = useAuthStore((s) => canManage(s.user?.role)); - const canManage = !!myUserId && (quotation.user_id === myUserId || isSuperAdmin); - const canNotify = canManage; // 초청 메일 발송/재발송 + const canMutate = !!myUserId && (quotation.user_id === myUserId || isSuperAdmin); + const canNotify = canMutate; // 초청 메일 발송/재발송 // 직접 낙찰 = 개찰(낙찰자 미정 마감) 견적에서만. 후보(투찰한 협상완료 협력사) 유무는 표에서 판정. - const canAward = canManage && chainRoundState(quotation) === 'opened'; + const canAward = canMutate && chainRoundState(quotation) === 'opened'; // 협상 세션·사용 카드는 견적 단위, 채팅은 선택 세션 단위로 서버에서 읽는다. // 세션은 협상 진행으로 계속 바뀌므로 탭 복귀 시 재조회한다. 카드는 생성 후 불변이라 끄둔다. const sessionsQuery = useGetQuotationSessions(qtId, { @@ -102,7 +102,7 @@ export function QuotationDetailSheet({ const { rounds: chainRounds, isLoading: chainLoading } = useQuotationChain(quotation.number); const maxRound = chainRounds.length ? Math.max(...chainRounds.map((r) => r.round)) : (quotation.round ?? 1); const isLatestRound = (quotation.round ?? 1) >= maxRound; - const canRegenerate = canManage && !chainLoading && quotation.status === QuotationStatus.CLOSED && isLatestRound; + const canRegenerate = canMutate && !chainLoading && quotation.status === QuotationStatus.CLOSED && isLatestRound; const [selectedSessionId, setSelectedSessionId] = useState(null); const effectiveSessionId = selectedSessionId ?? serverSessions[0]?.session_id ?? null; @@ -183,13 +183,13 @@ export function QuotationDetailSheet({ {/* 마감 버튼은 항상 노출하되, 본인/최고관리자가 아니거나 마감 가능한 상태가 아니면 비활성화한다. */} {(() => { const alreadyClosed = quotation.status === QuotationStatus.CLOSED; - const canClose = canManage && !alreadyClosed; + const canClose = canMutate && !alreadyClosed; return (