diff --git a/.env.example b/.env.example index d1e65ed6..182473ff 100644 --- a/.env.example +++ b/.env.example @@ -40,6 +40,15 @@ API_KEY=your-secret-api-key-here # 주의: 운영 환경에서는 반드시 false로 설정! NEXT_PUBLIC_DEV_TOOLBAR_ENABLED=false +# ============================================== +# Puppeteer (로컬 PDF 생성용) +# ============================================== +# puppeteer-core는 Chromium을 번들하지 않으므로 로컬 Chrome 경로 필요 +# macOS: /Applications/Google Chrome.app/Contents/MacOS/Google Chrome +# Linux: /usr/bin/google-chrome-stable +# Vercel에서는 @sparticuz/chromium이 자동 처리하므로 설정 불필요 +PUPPETEER_EXECUTABLE_PATH=/Applications/Google Chrome.app/Contents/MacOS/Google Chrome + # ============================================== # Development Notes # ============================================== diff --git a/claudedocs/vercel/vercel-env-setup-guide.md b/claudedocs/vercel/vercel-env-setup-guide.md new file mode 100644 index 00000000..37b7e706 --- /dev/null +++ b/claudedocs/vercel/vercel-env-setup-guide.md @@ -0,0 +1,81 @@ +# Vercel 환경변수 설정 가이드 + +## 설정 위치 +Vercel Dashboard → Project → Settings → Environment Variables + +--- + +## 필수 환경변수 + +### 서버 전용 (Server-side Only) + +| 변수명 | 설명 | 예시 값 | 환경 | +|--------|------|---------|------| +| `API_KEY` | 백엔드 API 인증 키 (X-API-KEY 헤더) | `42Jfwc6Ea...` | Production, Preview | +| `API_URL` | 백엔드 API 내부 URL (서버 사이드 전용) | `https://api.codebridge-x.com` | Production, Preview | + +> `API_KEY`는 절대 `NEXT_PUBLIC_` 접두사를 붙이지 말 것. 클라이언트에 노출되면 안 됨. + +### 클라이언트 공개 (NEXT_PUBLIC_*) + +| 변수명 | 설명 | Production 값 | Preview 값 | +|--------|------|--------------|------------| +| `NEXT_PUBLIC_API_URL` | 프론트엔드에서 호출하는 API URL | `https://api.codebridge-x.com` | `https://api.codebridge-x.com` | +| `NEXT_PUBLIC_FRONTEND_URL` | 프론트엔드 자체 URL (CORS 용) | `https://dev.codebridge-x.com` | Vercel 자동 할당 URL | +| `NEXT_PUBLIC_AUTH_MODE` | 인증 모드 | `sanctum` | `sanctum` | +| `NEXT_PUBLIC_GOOGLE_MAPS_API_KEY` | Google Maps API 키 | `AIzaSy...` | `AIzaSy...` | +| `NEXT_PUBLIC_DEV_TOOLBAR_ENABLED` | 개발 도구 툴바 | `false` | `true` | + +### 선택 환경변수 + +| 변수명 | 설명 | 기본값 | +|--------|------|--------| +| `NEXT_PUBLIC_API_LOGGING` | API 로깅 활성화 | 미설정 (비활성) | +| `NEXT_PUBLIC_APP_VERSION` | 앱 버전 표시 | 미설정 | +| `PUPPETEER_EXECUTABLE_PATH` | Chromium 경로 (Vercel에서는 불필요) | 미설정 | + +--- + +## 환경별 설정 방법 + +### 1. Vercel Dashboard에서 설정 + +1. [Vercel Dashboard](https://vercel.com) 접속 +2. 프로젝트 선택 → **Settings** → **Environment Variables** +3. 각 변수 추가 시 적용 환경 선택: + - **Production**: 운영 배포에만 적용 + - **Preview**: PR/브랜치 배포에 적용 + - **Development**: `vercel dev` 로컬 실행 시 적용 + +### 2. 환경별 권장 설정 + +#### Production +``` +API_KEY=<실제 운영 API 키> +API_URL=https://api.codebridge-x.com +NEXT_PUBLIC_API_URL=https://api.codebridge-x.com +NEXT_PUBLIC_FRONTEND_URL=https://<운영 도메인> +NEXT_PUBLIC_AUTH_MODE=sanctum +NEXT_PUBLIC_GOOGLE_MAPS_API_KEY= +NEXT_PUBLIC_DEV_TOOLBAR_ENABLED=false +``` + +#### Preview +``` +API_KEY=<개발용 API 키> +API_URL=https://api.codebridge-x.com +NEXT_PUBLIC_API_URL=https://api.codebridge-x.com +NEXT_PUBLIC_FRONTEND_URL= +NEXT_PUBLIC_AUTH_MODE=sanctum +NEXT_PUBLIC_GOOGLE_MAPS_API_KEY= +NEXT_PUBLIC_DEV_TOOLBAR_ENABLED=true +``` + +--- + +## 주의사항 + +1. **API_KEY 보안**: Sensitive 체크박스를 반드시 활성화하여 Dashboard에서도 값이 마스킹되도록 설정 +2. **NEXT_PUBLIC_ 접두사**: 이 접두사가 붙은 변수는 클라이언트 번들에 포함되므로 민감 정보 절대 금지 +3. **VERCEL 환경변수**: `VERCEL=1`은 Vercel이 자동 주입하므로 별도 설정 불필요 (PDF 생성 분기에 사용) +4. **NODE_ENV**: Vercel이 자동 설정 (`production` for Production, `development` for Preview) diff --git a/next.config.ts b/next.config.ts index 73a3226e..8d9d076a 100644 --- a/next.config.ts +++ b/next.config.ts @@ -6,7 +6,7 @@ const withNextIntl = createNextIntlPlugin('./src/i18n/request.ts'); const nextConfig: NextConfig = { reactStrictMode: false, // 🧪 TEST: Strict Mode 비활성화로 중복 요청 테스트 turbopack: {}, // ✅ CRITICAL: Next.js 15 + next-intl compatibility - serverExternalPackages: ['puppeteer'], // puppeteer는 Node.js 전용 - Webpack 번들 제외 + serverExternalPackages: ['puppeteer-core', '@sparticuz/chromium'], // Vercel 서버리스 PDF 생성용 - Webpack 번들 제외 images: { remotePatterns: [ { @@ -21,13 +21,9 @@ const nextConfig: NextConfig = { }, }, typescript: { - // ⚠️ WARNING: This allows production builds to complete even with TypeScript errors - // Only use during development. Remove for production deployments. - ignoreBuildErrors: true, + ignoreBuildErrors: false, }, eslint: { - // ⚠️ WARNING: Temporarily ignore ESLint during builds for migration - // TODO: Fix ESLint errors after migration is complete ignoreDuringBuilds: true, }, // Capacitor 패키지는 모바일 앱 전용 - 웹 빌드에서 제외 diff --git a/package-lock.json b/package-lock.json index d461bd7e..0588dc06 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,6 +29,7 @@ "@radix-ui/react-switch": "^1.2.6", "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-tooltip": "^1.2.8", + "@sparticuz/chromium": "^143.0.4", "@tiptap/extension-image": "^3.13.0", "@tiptap/extension-link": "^3.13.0", "@tiptap/extension-placeholder": "^3.13.0", @@ -48,7 +49,7 @@ "lucide-react": "^0.552.0", "next": "^15.5.9", "next-intl": "^4.4.0", - "puppeteer": "^23.11.1", + "puppeteer-core": "^24.37.2", "react": "^19.2.3", "react-day-picker": "^9.11.1", "react-dom": "^19.2.3", @@ -87,29 +88,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@babel/code-frame": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.28.6.tgz", - "integrity": "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==", - "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/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==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/runtime": { "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", @@ -1536,18 +1514,17 @@ } }, "node_modules/@puppeteer/browsers": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.6.1.tgz", - "integrity": "sha512-aBSREisdsGH890S2rQqK82qmQYU3uFpSH8wcZWHgHzl3LfzsxAKbLNiAG9mO8v1Y0UICBeClICxPJvyr0rcuxg==", + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.12.0.tgz", + "integrity": "sha512-Xuq42yxcQJ54ti8ZHNzF5snFvtpgXzNToJ1bXUGQRaiO8t+B6UM8sTUJfvV+AJnqtkJU/7hdy6nbKyA12aHtRw==", "license": "Apache-2.0", "dependencies": { - "debug": "^4.4.0", + "debug": "^4.4.3", "extract-zip": "^2.0.1", "progress": "^2.0.3", "proxy-agent": "^6.5.0", - "semver": "^7.6.3", - "tar-fs": "^3.0.6", - "unbzip2-stream": "^1.4.3", + "semver": "^7.7.3", + "tar-fs": "^3.1.1", "yargs": "^17.7.2" }, "bin": { @@ -2862,6 +2839,19 @@ "integrity": "sha512-bXHSaW5jRTmke9Vd0h5P7BtWZG9Znqb8gSDxZnxaGSJnGwPLDPfS+3g0BKzeWqzgZPsIVZkM7m2tbo18cm5HBw==", "license": "MIT" }, + "node_modules/@sparticuz/chromium": { + "version": "143.0.4", + "resolved": "https://registry.npmjs.org/@sparticuz/chromium/-/chromium-143.0.4.tgz", + "integrity": "sha512-/6I7uQTRhRDD2/gGPQ1Gkf+Dqk0RYDACPJDZfSzz0OWk4JmUTonNHPXbrn6UIklOHlnDLf8xAAzkOZKB/cJpLA==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "tar-fs": "^3.1.1" + }, + "engines": { + "node": ">=20.11.0" + } + }, "node_modules/@standard-schema/spec": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", @@ -4977,26 +4967,6 @@ "node": ">= 0.6.0" } }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "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/basic-ftp": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.1.0.tgz", @@ -5029,30 +4999,6 @@ "node": ">=8" } }, - "node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "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": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, "node_modules/buffer-crc32": { "version": "0.2.13", "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", @@ -5116,6 +5062,7 @@ "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" @@ -5192,22 +5139,22 @@ } }, "node_modules/chromium-bidi": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.11.0.tgz", - "integrity": "sha512-6CJWHkNRoyZyjV9Rwv2lYONZf1Xm0IuDyNq97nwSsxxP3wf5Bwy15K5rOvVKMtJ127jJBmxFUanSAOjgFRxgrA==", + "version": "13.1.1", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-13.1.1.tgz", + "integrity": "sha512-zB9MpoPd7VJwjowQqiW3FKOvQwffFMjQ8Iejp5ZW+sJaKLRhZX1sTxzl3Zt22TDB4zP0OOqs8lRoY7eAW5geyQ==", "license": "Apache-2.0", "dependencies": { - "mitt": "3.0.1", - "zod": "3.23.8" + "mitt": "^3.0.1", + "zod": "^3.24.1" }, "peerDependencies": { "devtools-protocol": "*" } }, "node_modules/chromium-bidi/node_modules/zod": { - "version": "3.23.8", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", - "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" @@ -5316,32 +5263,6 @@ "url": "https://opencollective.com/core-js" } }, - "node_modules/cosmiconfig": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", - "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", - "license": "MIT", - "dependencies": { - "env-paths": "^2.2.1", - "import-fresh": "^3.3.0", - "js-yaml": "^4.1.0", - "parse-json": "^5.2.0" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/d-fischer" - }, - "peerDependencies": { - "typescript": ">=4.9.5" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, "node_modules/crc-32": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", @@ -5700,9 +5621,9 @@ "license": "MIT" }, "node_modules/devtools-protocol": { - "version": "0.0.1367902", - "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1367902.tgz", - "integrity": "sha512-XxtPuC3PGakY6PD7dG66/o8KwJ/LkH2/EKe19Dcw58w53dv4/vSQEkn/SzuyhHE2q4zPgCkxQBxus3VV4ql+Pg==", + "version": "0.0.1566079", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1566079.tgz", + "integrity": "sha512-MJfAEA1UfVhSs7fbSQOG4czavUp1ajfg6prlAN0+cmfa2zNjaIbvq8VneP7do1WAQQIvgNJWSMeP6UyI90gIlQ==", "license": "BSD-3-Clause" }, "node_modules/doctrine": { @@ -5791,24 +5712,6 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, - "node_modules/env-paths": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", - "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/error-ex": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", - "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", - "license": "MIT", - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, "node_modules/es-abstract": { "version": "1.24.1", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz", @@ -6674,6 +6577,26 @@ "dev": true, "license": "ISC" }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/for-each": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", @@ -7078,26 +7001,6 @@ "node": ">= 14" } }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "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": "BSD-3-Clause" - }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -7122,6 +7025,7 @@ "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", @@ -7213,12 +7117,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "license": "MIT" - }, "node_modules/is-async-function": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", @@ -7655,12 +7553,14 @@ "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" @@ -7676,12 +7576,6 @@ "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", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "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", @@ -8047,12 +7941,6 @@ "url": "https://opencollective.com/parcel" } }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "license": "MIT" - }, "node_modules/linkify-it": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", @@ -8702,6 +8590,7 @@ "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" @@ -8710,24 +8599,6 @@ "node": ">=6" } }, - "node_modules/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -9150,40 +9021,19 @@ "node": ">=6" } }, - "node_modules/puppeteer": { - "version": "23.11.1", - "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-23.11.1.tgz", - "integrity": "sha512-53uIX3KR5en8l7Vd8n5DUv90Ae9QDQsyIthaUFVzwV6yU750RjqRznEtNMBT20VthqAdemnJN+hxVdmMHKt7Zw==", - "deprecated": "< 24.15.0 is no longer supported", - "hasInstallScript": true, - "license": "Apache-2.0", - "dependencies": { - "@puppeteer/browsers": "2.6.1", - "chromium-bidi": "0.11.0", - "cosmiconfig": "^9.0.0", - "devtools-protocol": "0.0.1367902", - "puppeteer-core": "23.11.1", - "typed-query-selector": "^2.12.0" - }, - "bin": { - "puppeteer": "lib/cjs/puppeteer/node/cli.js" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/puppeteer-core": { - "version": "23.11.1", - "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-23.11.1.tgz", - "integrity": "sha512-3HZ2/7hdDKZvZQ7dhhITOUg4/wOrDRjyK2ZBllRB0ZCOi9u0cwq1ACHDjBB+nX+7+kltHjQvBRdeY7+W0T+7Gg==", + "version": "24.37.2", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.37.2.tgz", + "integrity": "sha512-nN8qwE3TGF2vA/+xemPxbesntTuqD9vCGOiZL2uh8HES3pPzLX20MyQjB42dH2rhQ3W3TljZ4ZaKZ0yX/abQuw==", "license": "Apache-2.0", "dependencies": { - "@puppeteer/browsers": "2.6.1", - "chromium-bidi": "0.11.0", - "debug": "^4.4.0", - "devtools-protocol": "0.0.1367902", + "@puppeteer/browsers": "2.12.0", + "chromium-bidi": "13.1.1", + "debug": "^4.4.3", + "devtools-protocol": "0.0.1566079", "typed-query-selector": "^2.12.0", - "ws": "^8.18.0" + "webdriver-bidi-protocol": "0.4.0", + "ws": "^8.19.0" }, "engines": { "node": ">=18" @@ -9523,6 +9373,7 @@ "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" @@ -10277,12 +10128,6 @@ "utrie": "^1.0.2" } }, - "node_modules/through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", - "license": "MIT" - }, "node_modules/tiny-invariant": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", @@ -10517,16 +10362,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/unbzip2-stream": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", - "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==", - "license": "MIT", - "dependencies": { - "buffer": "^5.2.1", - "through": "^2.3.8" - } - }, "node_modules/undici-types": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", @@ -10695,6 +10530,12 @@ "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", "license": "MIT" }, + "node_modules/webdriver-bidi-protocol": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.0.tgz", + "integrity": "sha512-U9VIlNRrq94d1xxR9JrCEAx5Gv/2W7ERSv8oWRoNe/QYbfccS0V3h/H6qeNeCRJxXGMhhnkqvwNrvPAYeuP9VA==", + "license": "Apache-2.0" + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index 3b342d93..5cab0515 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "@radix-ui/react-switch": "^1.2.6", "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-tooltip": "^1.2.8", + "@sparticuz/chromium": "^143.0.4", "@tiptap/extension-image": "^3.13.0", "@tiptap/extension-link": "^3.13.0", "@tiptap/extension-placeholder": "^3.13.0", @@ -54,7 +55,7 @@ "lucide-react": "^0.552.0", "next": "^15.5.9", "next-intl": "^4.4.0", - "puppeteer": "^23.11.1", + "puppeteer-core": "^24.37.2", "react": "^19.2.3", "react-day-picker": "^9.11.1", "react-dom": "^19.2.3", diff --git a/src/app/[locale]/(protected)/quality/qms/components/InspectionModalV2.tsx b/src/app/[locale]/(protected)/quality/qms/components/InspectionModalV2.tsx index 006638c7..e910d9ef 100644 --- a/src/app/[locale]/(protected)/quality/qms/components/InspectionModalV2.tsx +++ b/src/app/[locale]/(protected)/quality/qms/components/InspectionModalV2.tsx @@ -210,9 +210,9 @@ const createQmsMockWorkOrder = (subType?: string): WorkOrder => ({ shutterCount: 5, department: '생산부', items: [ - { id: '1', no: 1, status: 'completed', productName: '와이어 스크린', floorCode: '1층/FSS-01', specification: '3000×2500', quantity: 2, unit: 'EA' }, - { id: '2', no: 2, status: 'in_progress', productName: '메쉬 스크린', floorCode: '2층/FSS-03', specification: '3000×2500', quantity: 3, unit: 'EA' }, - { id: '3', no: 3, status: 'waiting', productName: '광폭 와이어', floorCode: '3층/FSS-05', specification: '12000×4500', quantity: 1, unit: 'EA' }, + { id: '1', no: 1, status: 'completed', productName: '와이어 스크린', floorCode: '1층/FSS-01', specification: '3000×2500', quantity: 2, unit: 'EA', orderNodeId: null, orderNodeName: '' }, + { id: '2', no: 2, status: 'in_progress', productName: '메쉬 스크린', floorCode: '2층/FSS-03', specification: '3000×2500', quantity: 3, unit: 'EA', orderNodeId: null, orderNodeName: '' }, + { id: '3', no: 3, status: 'waiting', productName: '광폭 와이어', floorCode: '3층/FSS-05', specification: '12000×4500', quantity: 1, unit: 'EA', orderNodeId: null, orderNodeName: '' }, ], currentStep: 2, issues: [], @@ -314,7 +314,7 @@ export const InspectionModalV2 = ({ // 저장된 측정값을 initialValues로 변환 const docData = result.resolveData?.document?.data; if (docData && docData.length > 0) { - const values = parseSavedDataToInitialValues(tmpl, docData); + const values = parseSavedDataToInitialValues(tmpl, docData.map((d: { field_key: string; field_value?: string | null }) => ({ field_key: d.field_key, field_value: d.field_value ?? null }))); setImportInitialValues(values); } else { setImportInitialValues(undefined); diff --git a/src/app/[locale]/(protected)/quality/qms/mockData.ts b/src/app/[locale]/(protected)/quality/qms/mockData.ts index 3ec3cf84..cf6a0e07 100644 --- a/src/app/[locale]/(protected)/quality/qms/mockData.ts +++ b/src/app/[locale]/(protected)/quality/qms/mockData.ts @@ -13,6 +13,7 @@ export const MOCK_WORK_ORDER: WorkOrder = { projectName: '강남 아파트 단지', assignees: ['김작업', '이생산'], quantity: 5, + shutterCount: 3, dueDate: '2024-10-05', priority: 1, status: 'inProgress', diff --git a/src/app/api/pdf/generate/route.ts b/src/app/api/pdf/generate/route.ts index 38238644..edccdf0e 100644 --- a/src/app/api/pdf/generate/route.ts +++ b/src/app/api/pdf/generate/route.ts @@ -1,5 +1,6 @@ import { NextRequest, NextResponse } from 'next/server'; -import puppeteer from 'puppeteer'; +import puppeteer from 'puppeteer-core'; +import chromium from '@sparticuz/chromium'; /** * PDF 생성 API @@ -35,17 +36,20 @@ export async function POST(request: NextRequest) { ); } - // Puppeteer 브라우저 실행 (Docker Alpine에서는 시스템 Chromium 사용) + // 로컬 개발 vs Vercel 환경 분기 + const isVercel = process.env.VERCEL === '1'; const browser = await puppeteer.launch({ - headless: true, - executablePath: process.env.PUPPETEER_EXECUTABLE_PATH || undefined, - args: [ + args: isVercel ? chromium.args : [ '--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage', '--disable-gpu', '--disable-software-rasterizer', ], + executablePath: isVercel + ? await chromium.executablePath() + : process.env.PUPPETEER_EXECUTABLE_PATH || '/usr/bin/google-chrome-stable', + headless: true, }); const page = await browser.newPage(); diff --git a/src/components/material/ReceivingManagement/actions.ts b/src/components/material/ReceivingManagement/actions.ts index 4ac08f16..62d265eb 100644 --- a/src/components/material/ReceivingManagement/actions.ts +++ b/src/components/material/ReceivingManagement/actions.ts @@ -1325,10 +1325,10 @@ export async function getInspectionTemplate(params: { }, // 결재선 데이터 approvalLines: [ - { id: 1, role: '작성', sortOrder: 1 }, - { id: 2, role: '검토', sortOrder: 2 }, - { id: 3, role: '승인', sortOrder: 3 }, - { id: 4, role: '승인', sortOrder: 4 }, + { id: 1, name: '', dept: '', role: '작성', sortOrder: 1 }, + { id: 2, name: '', dept: '', role: '검토', sortOrder: 2 }, + { id: 3, name: '', dept: '', role: '승인', sortOrder: 3 }, + { id: 4, name: '', dept: '', role: '승인', sortOrder: 4 }, ], }, inspectionItems: [ diff --git a/src/components/orders/OrderSalesDetailView.tsx b/src/components/orders/OrderSalesDetailView.tsx index 4803d8e9..c643a6a0 100644 --- a/src/components/orders/OrderSalesDetailView.tsx +++ b/src/components/orders/OrderSalesDetailView.tsx @@ -133,16 +133,16 @@ function OrderNodeCard({ node, depth = 0 }: { node: OrderNode; depth?: number }) )} {node.name} - {options.product_name && ( + {options.product_name ? ( - ({options.product_name as string}) + ({String(options.product_name)}) - )} - {(options.open_width || options.open_height) && ( + ) : null} + {(options.open_width || options.open_height) ? ( - {options.open_width as string}x{options.open_height as string}mm + {String(options.open_width ?? '')}x{String(options.open_height ?? '')}mm - )} + ) : null}
diff --git a/src/components/production/WorkOrders/WorkOrderDetail.tsx b/src/components/production/WorkOrders/WorkOrderDetail.tsx index 906154ea..1456b607 100644 --- a/src/components/production/WorkOrders/WorkOrderDetail.tsx +++ b/src/components/production/WorkOrders/WorkOrderDetail.tsx @@ -231,6 +231,8 @@ export function WorkOrderDetail({ orderId }: WorkOrderDetailProps) { specification: '8,260 X 8,350 mm', quantity: 500, unit: 'm', + orderNodeId: null, + orderNodeName: '', }, { id: 'mock-2', @@ -241,6 +243,8 @@ export function WorkOrderDetail({ orderId }: WorkOrderDetailProps) { specification: '1,200 X 2,400 mm', quantity: 100, unit: 'EA', + orderNodeId: null, + orderNodeName: '', }, ]; } diff --git a/src/components/production/WorkOrders/WorkOrderEdit.tsx b/src/components/production/WorkOrders/WorkOrderEdit.tsx index 9788a28f..9a3da2bb 100644 --- a/src/components/production/WorkOrders/WorkOrderEdit.tsx +++ b/src/components/production/WorkOrders/WorkOrderEdit.tsx @@ -164,6 +164,8 @@ export function WorkOrderEdit({ orderId }: WorkOrderEditProps) { specification: '8,260 X 8,350 mm', quantity: 500, unit: 'm', + orderNodeId: null, + orderNodeName: '', }, { id: 'mock-2', @@ -174,6 +176,8 @@ export function WorkOrderEdit({ orderId }: WorkOrderEditProps) { specification: '1,200 X 2,400 mm', quantity: 100, unit: 'EA', + orderNodeId: null, + orderNodeName: '', }, ]); } else { diff --git a/src/components/production/WorkOrders/documents/InspectionReportModal.tsx b/src/components/production/WorkOrders/documents/InspectionReportModal.tsx index 73b511be..74a1d7a1 100644 --- a/src/components/production/WorkOrders/documents/InspectionReportModal.tsx +++ b/src/components/production/WorkOrders/documents/InspectionReportModal.tsx @@ -96,9 +96,9 @@ export function InspectionReportModal({ shutterCount: 12, department: '생산부', items: [ - { id: '1', no: 1, status: 'in_progress', productName: '와이어 스크린', floorCode: '1층/FSS-01', specification: '8,260 X 8,350', quantity: 2, unit: 'EA' }, - { id: '2', no: 2, status: 'waiting', productName: '메쉬 스크린', floorCode: '2층/FSS-03', specification: '6,400 X 5,200', quantity: 4, unit: 'EA' }, - { id: '3', no: 3, status: 'completed', productName: '광폭 와이어', floorCode: '3층/FSS-05', specification: '12,000 X 4,500', quantity: 1, unit: 'EA' }, + { id: '1', no: 1, status: 'in_progress', productName: '와이어 스크린', floorCode: '1층/FSS-01', specification: '8,260 X 8,350', quantity: 2, unit: 'EA', orderNodeId: null, orderNodeName: '' }, + { id: '2', no: 2, status: 'waiting', productName: '메쉬 스크린', floorCode: '2층/FSS-03', specification: '6,400 X 5,200', quantity: 4, unit: 'EA', orderNodeId: null, orderNodeName: '' }, + { id: '3', no: 3, status: 'completed', productName: '광폭 와이어', floorCode: '3층/FSS-05', specification: '12,000 X 4,500', quantity: 1, unit: 'EA', orderNodeId: null, orderNodeName: '' }, ], currentStep: 2, issues: [], diff --git a/src/components/production/WorkerScreen/WorkLogModal.tsx b/src/components/production/WorkerScreen/WorkLogModal.tsx index 331cff06..9f45474c 100644 --- a/src/components/production/WorkerScreen/WorkLogModal.tsx +++ b/src/components/production/WorkerScreen/WorkLogModal.tsx @@ -60,9 +60,9 @@ export function WorkLogModal({ open, onOpenChange, workOrderId, processType }: W shutterCount: 12, department: '생산부', items: [ - { id: '1', no: 1, status: 'in_progress', productName: '와이어 스크린', floorCode: '1층/FSS-01', specification: '8,260 X 8,350', quantity: 2, unit: 'EA' }, - { id: '2', no: 2, status: 'waiting', productName: '메쉬 스크린', floorCode: '2층/FSS-03', specification: '6,400 X 5,200', quantity: 4, unit: 'EA' }, - { id: '3', no: 3, status: 'completed', productName: '광폭 와이어', floorCode: '3층/FSS-05', specification: '12,000 X 4,500', quantity: 1, unit: 'EA' }, + { id: '1', no: 1, status: 'in_progress', productName: '와이어 스크린', floorCode: '1층/FSS-01', specification: '8,260 X 8,350', quantity: 2, unit: 'EA', orderNodeId: null, orderNodeName: '' }, + { id: '2', no: 2, status: 'waiting', productName: '메쉬 스크린', floorCode: '2층/FSS-03', specification: '6,400 X 5,200', quantity: 4, unit: 'EA', orderNodeId: null, orderNodeName: '' }, + { id: '3', no: 3, status: 'completed', productName: '광폭 와이어', floorCode: '3층/FSS-05', specification: '12,000 X 4,500', quantity: 1, unit: 'EA', orderNodeId: null, orderNodeName: '' }, ], currentStep: 2, issues: [], diff --git a/src/components/production/WorkerScreen/index.tsx b/src/components/production/WorkerScreen/index.tsx index 32712df5..dcabc45f 100644 --- a/src/components/production/WorkerScreen/index.tsx +++ b/src/components/production/WorkerScreen/index.tsx @@ -663,6 +663,7 @@ export default function WorkerScreen() { projectName: '-', assignees: [], quantity: mockItem.quantity, + shutterCount: 0, dueDate: '', priority: 5, status: 'waiting', @@ -789,6 +790,7 @@ export default function WorkerScreen() { projectName: '-', assignees: [], quantity: mockItem.quantity, + shutterCount: 0, dueDate: '', priority: 5, status: 'waiting', @@ -814,6 +816,7 @@ export default function WorkerScreen() { projectName: '-', assignees: [], quantity: item.quantity, + shutterCount: 0, dueDate: '', priority: 5, status: 'waiting', diff --git a/src/components/quotes/LocationDetailPanel.tsx b/src/components/quotes/LocationDetailPanel.tsx index 5f7a0ef8..aadbde44 100644 --- a/src/components/quotes/LocationDetailPanel.tsx +++ b/src/components/quotes/LocationDetailPanel.tsx @@ -154,9 +154,10 @@ export function LocationDetailPanel({ Object.entries(subtotals).forEach(([key, value]) => { if (typeof value === "object" && value !== null) { + const obj = value as { name?: string }; tabs.push({ value: key, - label: value.name || key, + label: obj.name || key, }); } }); diff --git a/src/components/quotes/QuotePreviewContent.tsx b/src/components/quotes/QuotePreviewContent.tsx index d5c4ad67..a3318141 100644 --- a/src/components/quotes/QuotePreviewContent.tsx +++ b/src/components/quotes/QuotePreviewContent.tsx @@ -10,6 +10,7 @@ import React from 'react'; import type { QuoteFormDataV2 } from './QuoteRegistrationV2'; +import type { BomCalculationResultItem } from './types'; // 양식 타입 type TemplateType = 'vendor' | 'calculation'; @@ -327,7 +328,7 @@ export function QuotePreviewContent({ {/* BOM 품목 상세 */} {bomItems.length > 0 ? ( - bomItems.map((item, itemIndex) => ( + bomItems.map((item: BomCalculationResultItem, itemIndex: number) => ( {itemIndex === 0 ? locationSymbol : ''} diff --git a/src/components/quotes/QuoteSummaryPanel.tsx b/src/components/quotes/QuoteSummaryPanel.tsx index ce50a5ca..e641e0f6 100644 --- a/src/components/quotes/QuoteSummaryPanel.tsx +++ b/src/components/quotes/QuoteSummaryPanel.tsx @@ -108,10 +108,11 @@ export function QuoteSummaryPanel({ unitPrice: item.unit_price || 0, totalPrice: item.total_price || 0, })); + const obj = value as { name?: string; count?: number; subtotal?: number }; result.push({ - label: value.name || key, - count: value.count || 0, - amount: value.subtotal || 0, + label: obj.name || key, + count: obj.count || 0, + amount: obj.subtotal || 0, items: groupItems, }); } else if (typeof value === "number") { diff --git a/src/components/quotes/actions.ts b/src/components/quotes/actions.ts index 9e199b20..0ccdd67b 100644 --- a/src/components/quotes/actions.ts +++ b/src/components/quotes/actions.ts @@ -32,6 +32,7 @@ import type { BomCalculationResultItem, BomCalculationResult, } from './types'; +export type { BomCalculationResult, BomCalculationResultItem }; import { transformApiToFrontend, transformFrontendToApi } from './types'; // ===== 페이지네이션 타입 ===== diff --git a/vercel.json b/vercel.json new file mode 100644 index 00000000..e62f2c3e --- /dev/null +++ b/vercel.json @@ -0,0 +1,9 @@ +{ + "regions": ["icn1"], + "functions": { + "src/app/api/pdf/generate/route.ts": { + "memory": 1024, + "maxDuration": 30 + } + } +}