diff --git a/.codex/hooks.json b/.codex/hooks.json
new file mode 100644
index 00000000..6bf4e05e
--- /dev/null
+++ b/.codex/hooks.json
@@ -0,0 +1,15 @@
+{
+ "hooks": {
+ "PreToolUse": [
+ {
+ "matcher": "Bash",
+ "hooks": [
+ {
+ "type": "command",
+ "command": "CMD=$(python3 -c \"import json,sys; d=json.load(sys.stdin); print(d.get('tool_input',d).get('command',''))\" 2>/dev/null || true); set -- $CMD; case \"$1\" in grep|egrep|fgrep|rg|ripgrep|find|fd|ack|ag) [ -f graphify-out/graph.json ] && echo '{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"additionalContext\":\"graphify: Knowledge graph exists. Read graphify-out/GRAPH_REPORT.md for god nodes and community structure before searching raw files.\"}}' || true ;; esac"
+ }
+ ]
+ }
+ ]
+ }
+}
diff --git a/.env.development b/.env.development
index 357a2f7f..6b3844f8 100644
--- a/.env.development
+++ b/.env.development
@@ -33,4 +33,8 @@ NEXT_PUBLIC_AWS_S3_BASE_URL="//files.hanasys.jp"
S3_PROFILE="dev"
#logging
-NEXT_PUBLIC_ENABLE_LOGGING=true
\ No newline at end of file
+NEXT_PUBLIC_ENABLE_LOGGING=true
+
+#Gemini API Key
+GEMINI_API_KEY="AIzaSyDDmMkTkNt12QTj7I3SUygYkOoeG_Sc28Q"
+GEMINI_MODEL="gemini-3.1-pro-preview"
\ No newline at end of file
diff --git a/.env.local.dev b/.env.local.dev
index ab789dbf..a53524ca 100644
--- a/.env.local.dev
+++ b/.env.local.dev
@@ -29,4 +29,8 @@ AWS_ACCESS_KEY_ID="AKIA3K4QWLZHFZRJOM2E"
AWS_SECRET_ACCESS_KEY="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"
NEXT_PUBLIC_AWS_S3_BASE_URL="//files.hanasys.jp"
-S3_PROFILE="dev"
\ No newline at end of file
+S3_PROFILE="dev"
+
+#Gemini API Key
+GEMINI_API_KEY="AIzaSyDDmMkTkNt12QTj7I3SUygYkOoeG_Sc28Q"
+GEMINI_MODEL="gemini-3.1-pro-preview"
\ No newline at end of file
diff --git a/.env.localhost b/.env.localhost
index 3532ed1f..7cab2757 100644
--- a/.env.localhost
+++ b/.env.localhost
@@ -35,3 +35,7 @@ S3_PROFILE="dev"
#logging
# [debug auto-capture 2026-05-06] true 로 켜면 debugCapture 가 debug/*.json/*.log 로 자동 저장.
NEXT_PUBLIC_ENABLE_LOGGING=true
+
+#Gemini API Key
+GEMINI_API_KEY="AIzaSyDDmMkTkNt12QTj7I3SUygYkOoeG_Sc28Q"
+GEMINI_MODEL="gemini-3.1-pro-preview"
diff --git a/.env.production b/.env.production
index 6e2981d9..50904369 100644
--- a/.env.production
+++ b/.env.production
@@ -32,4 +32,8 @@ NEXT_PUBLIC_AWS_S3_BASE_URL="//files.hanasys.jp"
S3_PROFILE="prd"
#logging
-NEXT_PUBLIC_ENABLE_LOGGING=false
\ No newline at end of file
+NEXT_PUBLIC_ENABLE_LOGGING=false
+
+#Gemini API Key
+GEMINI_API_KEY="AIzaSyDDmMkTkNt12QTj7I3SUygYkOoeG_Sc28Q"
+GEMINI_MODEL="gemini-3.1-pro-preview"
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 7f720b6a..7a1234d4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -56,12 +56,16 @@ sl
# [graphify 2026-05-12] graphify 지식 그래프 산출물 (로컬 캐시, post-commit hook 으로 자동 갱신)
graphify-out/
+# [playwright-mcp] Playwright MCP 임시 산출물 (스크린샷, 콘솔 로그)
+.playwright-mcp/
+
# [local-only 2026-05-08] 모든 markdown 문서는 git 추적 제외 (로컬 전용)
-# 단, 루트 README.md / CLAUDE.md / AGENTS.md, .agents 및 .claude 하위의 md 는 추적 유지
+# 단, 루트 README.md / CLAUDE.md / AGENTS.md, docs, .agents 및 .claude 하위의 md 는 추적 유지
*.md
!/README.md
!/CLAUDE.md
!/AGENTS.md
+!/docs/**/*.md
!.agents/**/*.md
!.claude/**/*.md
diff --git a/docs/PDF Gemini 평면도 분석 기능 구현 계획.html b/docs/PDF Gemini 평면도 분석 기능 구현 계획.html
new file mode 100644
index 00000000..a401e541
--- /dev/null
+++ b/docs/PDF Gemini 평면도 분석 기능 구현 계획.html
@@ -0,0 +1,1071 @@
+
+
+
+
+
+PDF 평면도 → Gemini 분석 → 캔버스 반영 · 구현 계획
+
+
+
+
+
+
+
+
+
+
+
+ Engineering Spec · Floor Plan / PDF Import
+ PDF 평면도 → Gemini 분석 → 캔버스 반영 구현 계획
+
+ 사용자가 업로드한 평면도 PDF 를 Gemini 멀티모달 모델로 분석해
+ 건물 외곽선 폴리곤 좌표(mm)를 얻고, Fabric.js 기반 floor-plan
+ 캔버스에 자동으로 반영한다. 업로드 파일은 분석 종료와 동시에
+ Gemini File API 측에서 폐기한다.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
01
+
+
Requirements
+
기능 요구 사항
+
+
+
+
REQ-01 기본 프로세스 상에서 특정 버튼 클릭으로 사용자가 PDF 문서를 업로드한다.
+
REQ-02 Gemini API 로 해당 문서의 내용 중 평면도를 분석한다.
+
REQ-03 분석 내용을 응답으로 전달받고 현재 프로세스(캔버스)로 전달한다.
+
REQ-04 분석이 성공적으로 마무리되면 업로드한 파일은 폐기 처리한다.
+
+
+
+
+
+
+
+
워크트리
현재 워크트리 lucid-shaw-91e51f 에서 진행
+
캔버스 반영 형태
외곽선 폴리곤 좌표(JSON)
+
+
API 호출 위치
서버 라우트 /api/gemini/floor-plan
+
Gemini 모델
gemini-3.1-pro-preview
+
+
+
+
+
+
+
+
+
03
+
+
End-to-end Flow
+
전체 흐름
+
+
+
+
+
+
+ Client · UI
+ 툴바 버튼 클릭
+
+
+
+
+
+ Client · Modal
+ PdfImportModal — 파일 선택 및 업로드
+
+
+
+
+
+ Server · Route
+ POST /api/gemini/floor-plan (multipart)
+
+
+
+
+
+ Gemini · File API
+ PDF 업로드 → fileUri 획득
+
+
+
+
+
+ Gemini · gemini-3.1-pro-preview
+ 외곽선 좌표 JSON 응답 (mm 단위)
+
+
+
+
+
+ Server · finally
+ Gemini 파일 삭제 (성공 / 실패 무관)
+
+
+
+
+
+ Client · Canvas
+ 좌표 검증 → outerLinePointsState + QLine / QPolygon 생성
+
+
+
+ ∗ 클라이언트는 서버 라우트만 호출하며 Gemini API 키와 직접 통신하지 않는다.
+
+
+
+
+
+
04
+
+
Files
+
핵심 파일 (수정 / 신규)
+
+
+
+
+
+
+
서버 라우트
+
src/app/api/gemini/floor-plan/route.js
+
multipart 수신 + Gemini 호출 + finally 폐기
+
신규
+
+
+
환경설정
+
src/config/config.*.js
+
GEMINI_API_KEY, GEMINI_MODEL 추가
+
수정
+
+
+
의존성
+
package.json
+
@google/generative-ai 추가
+
수정
+
+
+
메뉴 등록
+
src/store/menuAtom.js (menusState)
+
{ type: 'pdf-import', name, icon } 항목
+
수정
+
+
+
툴바 분기
+
src/components/floor-plan/CanvasMenu.jsx (onClickNav)
+
case 'pdf-import' 추가
+
수정
+
+
+
업로드 모달
+
src/components/floor-plan/modal/pdfImport/PdfImportModal.jsx
+
파일 선택 / 진행 / 에러 상태머신
+
신규
+
+
+
캔버스 반영 hook
+
src/hooks/pdf-import/usePdfImport.js
+
좌표 검증 + 단위 변환 + 캔버스 반영
+
신규
+
+
+
다국어 메시지
+
메시지 키 추가 — pdf.import.*
+
ja / ko 양쪽
+
수정
+
+
+
아이콘
+
public/ 또는 기존 아이콘 디렉토리
+
PDF 임포트용 SVG 추가
+
신규
+
+
+
+
+
+
+
+
05
+
+
Server Route · /api/gemini/floor-plan
+
서버 라우트 설계
+
+
+
+
+
+
5 · 1
입력 (Input)
+
+ Content-Type · multipart/form-data
+ file · PDF (≤ 20MB, MIME application/pdf)
+ unitHint (옵션) · mm · auto
+
+
+
+
+
5 · 2
처리 (Pipeline)
+
+ formData.get('file') → 사이즈 / MIME 검증
+ GoogleGenerativeAI(GEMINI_API_KEY) 클라이언트 생성
+ Gemini File API 업로드 → fileUri 획득
+ gemini-3.1-pro-preview 호출 · 외곽선 추출 프롬프트 + JSON 강제
+ 응답 JSON 파싱 + 좌표 검증 (vertex ≥ 3, 자기교차 / 0면적 거부)
+ finally · genai.files.delete(uploadedFile.name)
+
+
+
+
+
5 · 3
출력 (Output)
+
+ 성공 · { outerline:[{x,y},…], unit:'mm', confidence }
+ 실패 · { error:{ code, message } }
+ 응답 스키마: { outerline, unit:'mm', scale?, confidence:0..1, notes? }
+ response_mime_type=application/json · response_schema 강제
+
+
+
+
+
5 · 4
보안 / 운영 (Ops)
+
+ GEMINI_API_KEY 는 서버 env 만 사용 · 클라이언트 노출 금지
+ S3 등 영구 저장소에 절대 저장하지 않음 (REQ-04 폐기)
+ 로깅은 src/util/logger.js 만 사용
+ raw 응답 출력은 NEXT_PUBLIC_ENABLE_LOGGING=true 일 때만
+
+
+
+
+
+
+
+
+
06
+
+
Client Flow
+
클라이언트 흐름
+
+
+
+
+
+
6 · 1 모달 PdfImportModal
+
+ 상태머신 · idle → uploading → analyzing → success | error
+ 호출 · fetch('/api/gemini/floor-plan', { method:'POST', body: formData })
+ 성공 시 usePdfImport.applyOuterline(points, unit) 호출 후 모달 닫기
+ 실패 시 useSwal 로 에러 메시지 표시
+
+
+
+
6 · 2 캔버스 반영 hook usePdfImport
+
+ 단위 변환 · unit === 'mm' → canvas px/mm 비율로 변환
+ 정규화 · 시계방향 정렬 + bounding box 중앙 정렬(translate)
+ 기존 외곽선 존재 시 useSwal confirm — 덮어쓰기 분기
+ 점 → name === 'outerLine' QLine 객체 add
+ setOuterLinePoints(points) 후 addPolygonByLines(lines) 재사용
+ canvas.renderAll()
+
+
+
+
+
+
+
+
+
07
+
+
Edge Cases
+
검증 / 엣지 케이스
+
+
+
+
+
+
평면도가 아닌 PDF
모델이 빈 outerline 반환 → 친화적 메시지
+
좌표 ≤ 2개 / 자기교차 / 0면적
라우트에서 거부 (SizeSetting 가드와 일관)
+
모델 JSON 파싱 실패
retry 1회 후 실패 메시지
+
Gemini 호출 실패
finally 에서 File API 삭제 보장
+
캔버스 기존 외곽선 존재
confirm 후 덮어쓰기
+
응답이 거대한 폴리곤(> 500 vertex)
Douglas–Peucker 단순화 또는 거부
+
업로드 파일 크기 초과
클라이언트 / 서버 양쪽 가드
+
네트워크 실패
모달에 재시도 버튼 + 파일 폐기 보장
+
+
+
+
+
+
+
08
+
+
Conventions · Ops
+
컨벤션 / 운영
+
+
+
+
+
+
Formatting
+
+ Prettier · single quote · no semicolons
+ tabWidth: 2 · printWidth: 150
+ 경로 alias @/... 우선 (상대경로 체이닝 지양)
+
+
+
+
Messaging / I18n
+
+ 모든 알림 · 다이얼로그는 useSwal + useMessage
+ raw alert() 금지
+ 다국어 키 pdf.import.* ja / ko 양쪽 추가
+
+
+
+
Logging
+
+ console.log 직접 사용 금지
+ src/util/logger.js · NEXT_PUBLIC_ENABLE_LOGGING 게이트
+ Gemini raw 응답은 로깅 플래그 ON 시에만 출력
+
+
+
+
Verification
+
+ 마지막에 lint / type / build subagent 점검
+ dev 에서 실제 평면도 PDF 1~2건 manual test
+
+
+
+
+
+
[xxxx] feat(api) — Gemini 평면도 분석 라우트 신설
+
[xxxx] feat(floor-plan) — PDF 임포트 모달 / 툴바 진입
+
[xxxx] feat(floor-plan) — 분석 결과 외곽선 캔버스 반영
+
[xxxx] chore — 다국어 메시지 / lint 정리
+
+
+
+
+
+
+
+
+
+
@google/generative-ai 의존성 추가
+
GEMINI_API_KEY · GEMINI_MODEL env / config 매핑
+
+
+
서버 라우트 src/app/api/gemini/floor-plan/route.js 작성
+
Gemini 호출 + 파일 폐기 (try / finally)
+
+
+
menusState 에 pdf-import 항목 + PDF 아이콘 SVG 추가
+
기존 menusState 아이콘 컨벤션에 맞춤
+
+
+
CanvasMenu.jsx onClickNav switch 에 케이스 추가
+
modalAtom 으로 PdfImportModal 오픈
+
+
+
PdfImportModal.jsx 작성
+
idle → uploading → analyzing → success / error 상태머신
+
+
+
usePdfImport.js 작성
+
단위 / 방향 정규화 · 외곽선 덮어쓰기 분기 · QLine / QPolygon 반영
+
+
+
다국어 메시지 키 추가
+
pdf.import.* · ja / ko 양쪽
+
+
+
yarn lint + dev 환경 manual test
+
실제 평면도 PDF 1~2건으로 회귀 확인
+
+
+
의미 단위로 커밋
+
§ 8 의 commit 단위 표 참고
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/PDF Gemini 평면도 분석 기능 구현 계획.md b/docs/PDF Gemini 평면도 분석 기능 구현 계획.md
new file mode 100644
index 00000000..7a5db65a
--- /dev/null
+++ b/docs/PDF Gemini 평면도 분석 기능 구현 계획.md
@@ -0,0 +1,166 @@
+# PDF 평면도 → Gemini 분석 → 캔버스 반영 구현 계획
+
+> 작성일: 2026-05-18
+> 대상 브랜치: `claude/lucid-shaw-91e51f` (dev `f32f9187` 분기)
+> 모델: `gemini-3.1-pro-preview` · 좌표 단위: `mm` · 진입 위치: floor-plan 상단 툴바
+
+---
+
+## 1. 기능 요구 사항
+
+1. 기본 프로세스 상에서 특정 버튼 클릭으로 사용자가 PDF 문서를 업로드한다.
+2. Gemini API 로 해당 문서의 내용 중 평면도를 분석한다.
+3. 분석 내용을 응답으로 전달받고 현재 프로세스(캔버스)로 전달한다.
+4. 분석이 성공적으로 마무리되면 업로드한 파일은 폐기 처리한다.
+
+---
+
+## 2. 결정사항
+
+| 항목 | 선택 |
+|---|---|
+| 워크트리 | 현재 워크트리(`lucid-shaw-91e51f`)에서 진행 |
+| 캔버스 반영 형태 | 외곽선 폴리곤 좌표(JSON) |
+| 버튼 위치 | floor-plan 상단 툴바 |
+| API 호출 위치 | 서버 라우트 (`/api/gemini/floor-plan`) |
+| Gemini 모델 | `gemini-3.1-pro-preview` |
+| 좌표 단위 | `mm` |
+| 툴바 아이콘 | 신규 PDF 아이콘 추가 |
+
+---
+
+## 3. 전체 흐름
+
+```
+[툴바 버튼] → [PdfImportModal] → POST /api/gemini/floor-plan (multipart)
+ ↓
+ Gemini File API 업로드
+ ↓
+ 외곽선 좌표 JSON 응답
+ ↓
+ Gemini 파일 삭제 (finally)
+ ↓
+ 응답 좌표 → 좌표 검증/정규화 → outerLinePointsState + QLine/QPolygon 생성
+```
+
+---
+
+## 4. 핵심 파일 (수정/신규)
+
+| 영역 | 경로 | 변경 |
+|---|---|---|
+| 서버 라우트 | `src/app/api/gemini/floor-plan/route.js` | **신규** — multipart 수신 + Gemini 호출 + finally 폐기 |
+| 환경설정 | `src/config/config.*.js` | `GEMINI_API_KEY`, `GEMINI_MODEL` 추가 |
+| 의존성 | `package.json` | `@google/generative-ai` 추가 |
+| 메뉴 등록 | `src/store/menuAtom.js` (`menusState`) | `{ type: 'pdf-import', name, icon }` 항목 |
+| 툴바 분기 | `src/components/floor-plan/CanvasMenu.jsx` (`onClickNav`) | `case 'pdf-import'` 추가 |
+| 업로드 모달 | `src/components/floor-plan/modal/pdfImport/PdfImportModal.jsx` | **신규** — 파일 선택/진행/에러 |
+| 캔버스 반영 hook | `src/hooks/pdf-import/usePdfImport.js` | **신규** — 좌표 검증 + 캔버스 반영 |
+| 다국어 메시지 | 메시지 키 추가 | `pdf.import.*` (ja/ko) |
+| 아이콘 | `public/` 또는 기존 아이콘 디렉토리 | PDF 임포트용 SVG 추가 |
+
+---
+
+## 5. 서버 라우트 설계 (`/api/gemini/floor-plan`)
+
+### 5-1. 입력
+- `multipart/form-data`
+- 필드:
+ - `file`: PDF (≤ 20MB, MIME `application/pdf`)
+ - `unitHint` (옵션): `mm` | `auto`
+
+### 5-2. 처리 단계
+1. `formData.get('file')` 로 PDF 수신 → 사이즈/MIME 검증
+2. `GoogleGenerativeAI(GEMINI_API_KEY)` 클라이언트 생성
+3. **Gemini File API 업로드** → `fileUri` 획득
+4. 모델 호출 (`gemini-3.1-pro-preview`)
+ - 프롬프트 핵심:
+ - 역할: "건축 평면도 외곽선 추출 전문가"
+ - 출력: JSON only, 단위 mm
+ - 스키마: `{ outerline: [{x,y}, ...], unit: 'mm', scale?: number, confidence: 0..1, notes?: string }`
+ - "평면도가 아니면 `{ outerline: [], notes: '...' }` 반환"
+ - `response_mime_type=application/json`, `response_schema` 강제
+5. JSON 파싱 → 좌표 검증
+ - vertex ≥ 3
+ - 자기교차 검출 (turf 활용)
+ - 0 면적 거부
+6. **finally** 블록에서 `genai.files.delete(uploadedFile.name)` — 성공/실패 무관 삭제
+
+### 5-3. 출력
+- 성공: `{ outerline: [{x,y}, ...], unit: 'mm', confidence }`
+- 실패: `{ error: { code, message } }`
+
+### 5-4. 보안 / 운영
+- `GEMINI_API_KEY` 는 서버 env 만 사용, 클라이언트 노출 금지
+- S3 등 영구 저장소에 절대 저장하지 않음 (요구 4번: 폐기)
+- 로깅: `src/util/logger.js` 만 사용, raw 응답은 `NEXT_PUBLIC_ENABLE_LOGGING=true` 일 때만 출력
+
+---
+
+## 6. 클라이언트 흐름
+
+### 6-1. 모달 (`PdfImportModal`)
+- 상태머신: `idle → uploading → analyzing → success | error`
+- 호출: `fetch('/api/gemini/floor-plan', { method: 'POST', body: formData })`
+- 성공: `usePdfImport.applyOuterline(points, unit)` 호출 후 모달 닫기
+- 실패: `useSwal` 로 에러 메시지 표시
+
+### 6-2. 캔버스 반영 hook (`usePdfImport`)
+- **단위 변환**: `unit === 'mm'` 응답 → canvas px/mm 비율로 변환
+- **정규화**: 시계방향 정렬, bounding box 중앙 정렬(translate)
+- **기존 외곽선 분기**:
+ - 존재 시 `useSwal` confirm: "기존 외곽선을 덮어쓰시겠습니까?"
+ - 빈 캔버스 시 즉시 적용
+- **적용**:
+ - 점들을 `name === 'outerLine'` QLine 객체들로 생성하여 캔버스에 add
+ - `setOuterLinePoints(points)` (recoil `outerLinePointsState`)
+ - `addPolygonByLines(lines)` 흐름으로 QPolygon 생성 (기존 `useRoofShapeSetting` 자산 재사용)
+ - `canvas.renderAll()`
+
+---
+
+## 7. 검증 / 엣지 케이스
+
+| 케이스 | 대응 |
+|---|---|
+| PDF 가 아닌 파일 | 라우트에서 MIME 거부 |
+| 평면도가 아닌 PDF | 모델이 빈 outerline 반환 → 친화적 메시지 |
+| 좌표 ≤ 2개 / 자기교차 / 0면적 | 라우트에서 거부 (`SizeSetting` 가드와 일관) |
+| 모델 JSON 파싱 실패 | retry 1회 후 실패 메시지 |
+| Gemini 호출 실패 | finally 에서 File API 삭제 보장 |
+| 캔버스 기존 외곽선 존재 | confirm 후 덮어쓰기 |
+| 응답이 거대한 폴리곤(>500 vertex) | Douglas–Peucker 단순화 또는 거부 |
+| 업로드 파일 크기 초과 | 클라이언트/서버 양쪽 가드 |
+| 네트워크 실패 | 모달에 재시도 버튼 + 파일 폐기 보장 |
+
+---
+
+## 8. 컨벤션 / 운영
+
+- Prettier: single quote, **no semicolons**, tabWidth 2, printWidth 150
+- 메시지/모달: `useSwal` + `useMessage` (raw `alert()` 금지)
+- 다국어: `ja/ko` 양쪽 키 추가 (`pdf.import.*`)
+- 경로 alias: `@/...` 우선
+- 로깅: `src/util/logger.js` 만 사용
+
+### 커밋 단위
+1. `[xxxx] feat(api) — Gemini 평면도 분석 라우트 신설`
+2. `[xxxx] feat(floor-plan) — PDF 임포트 모달/툴바 진입`
+3. `[xxxx] feat(floor-plan) — 분석 결과 외곽선 캔버스 반영`
+4. `[xxxx] chore — 다국어 메시지/lint 정리`
+
+마지막에 lint/type/build subagent 점검.
+
+---
+
+## 9. 작업 순서 (To-Do)
+
+1. `@google/generative-ai` 의존성 추가, `GEMINI_API_KEY`/`GEMINI_MODEL` env·config 매핑
+2. 서버 라우트 `src/app/api/gemini/floor-plan/route.js` 작성 (Gemini 호출 + 파일 폐기)
+3. `menusState` 에 `pdf-import` 항목 + PDF 아이콘 SVG 추가
+4. `CanvasMenu.jsx` `onClickNav` switch 에 케이스 추가, `modalAtom` 으로 모달 오픈
+5. `PdfImportModal.jsx` 작성 (idle→uploading→analyzing→success/error 상태머신)
+6. `usePdfImport.js` 작성 (단위/방향 정규화, 외곽선 덮어쓰기 분기, QLine/QPolygon 반영)
+7. 다국어 메시지 키 추가 (ja/ko)
+8. `yarn lint` + dev에서 실제 PDF 한두 건 manual test
+9. 의미 단위로 커밋
diff --git a/package.json b/package.json
index 1f96d08c..e990626b 100644
--- a/package.json
+++ b/package.json
@@ -14,11 +14,13 @@
"start:cluster1": "env-cmd -f .env.production next start -p 5000",
"start:cluster2": "env-cmd -f .env.production next start -p 5001",
"start:dev": "env-cmd -f .env.development next start -p 5010",
+ "start:btob": "env-cmd -f .env.development next start -p 5060",
"lint": "next lint",
"serve": "node server.js"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.772.0",
+ "@google/generative-ai": "^0.24.1",
"ag-grid-react": "^32.0.2",
"axios": "^1.7.8",
"big.js": "^6.2.2",
diff --git a/src/app/api/gemini/floor-plan/route.js b/src/app/api/gemini/floor-plan/route.js
new file mode 100644
index 00000000..0d34a90e
--- /dev/null
+++ b/src/app/api/gemini/floor-plan/route.js
@@ -0,0 +1,210 @@
+import { NextResponse } from 'next/server'
+import { GoogleGenerativeAI, SchemaType } from '@google/generative-ai'
+import { GoogleAIFileManager, FileState } from '@google/generative-ai/server'
+
+import { logger } from '@/util/logger'
+
+const MAX_FILE_BYTES = 20 * 1024 * 1024 // 20MB
+const MAX_VERTICES = 500
+const DEFAULT_MODEL = 'gemini-3.1-pro-preview'
+
+const FLOOR_PLAN_SCHEMA = {
+ type: SchemaType.OBJECT,
+ properties: {
+ outerline: {
+ type: SchemaType.ARRAY,
+ items: {
+ type: SchemaType.OBJECT,
+ properties: {
+ x: { type: SchemaType.NUMBER },
+ y: { type: SchemaType.NUMBER },
+ },
+ required: ['x', 'y'],
+ },
+ },
+ unit: { type: SchemaType.STRING },
+ scale: { type: SchemaType.NUMBER },
+ confidence: { type: SchemaType.NUMBER },
+ notes: { type: SchemaType.STRING },
+ },
+ required: ['outerline', 'unit'],
+}
+
+const BASE_PROMPT = `당신은 건축 평면도(주택 도면)의 외곽선 추출 전문가입니다.
+첨부된 PDF에서 건물의 외곽(외벽선) 폴리곤 좌표를 추출하여 JSON 으로만 응답하세요.
+
+규칙:
+1. 모든 좌표는 밀리미터(mm) 단위의 실측 치수로 환산해야 합니다. 도면의 축척(scale)을 식별하여 적용하세요.
+2. 좌표계는 도면의 좌상단을 원점으로 하고, x 는 우측 양수, y 는 하단 양수로 합니다.
+3. 외곽선은 시계 반대 방향(CCW) 으로 정렬된 정점 배열로 반환합니다.
+4. 자기교차가 없는 단순 폴리곤이어야 하며, 정점은 3개 이상이어야 합니다.
+5. PDF 가 평면도가 아니거나 외곽을 추출할 수 없으면 outerline 을 빈 배열로 반환하고 notes 에 사유를 적습니다.
+6. 응답은 반드시 단일 JSON 객체만 포함하며, 다른 텍스트, 마크다운, 코드펜스를 포함하지 마세요.
+7. 좌표는 정수 또는 소수 첫째 자리까지 허용합니다.
+
+스키마:
+{
+ "outerline": [{"x": number, "y": number}, ...],
+ "unit": "mm",
+ "scale": number (선택, 도면 1mm 당 실제 mm),
+ "confidence": number (0~1),
+ "notes": string (선택)
+}`
+
+const buildPrompt = ({ pageMode, facadePages, floorPages } = {}) => {
+ if (pageMode === 'specify' && (facadePages || floorPages)) {
+ const hints = []
+ if (floorPages) hints.push(`평면도(平面図) 페이지: ${floorPages}`)
+ if (facadePages) hints.push(`입면도(立面図) 페이지: ${facadePages}`)
+ return `${BASE_PROMPT}\n\n페이지 힌트: ${hints.join(' / ')}. 외곽선은 위에서 지정한 평면도 페이지 기준으로 추출하세요.`
+ }
+ return BASE_PROMPT
+}
+
+const validateOuterline = (outerline) => {
+ if (!Array.isArray(outerline)) {
+ return { ok: false, code: 'INVALID_FORMAT', message: 'outerline 이 배열이 아닙니다.' }
+ }
+ if (outerline.length === 0) {
+ return { ok: true, empty: true }
+ }
+ if (outerline.length < 3) {
+ return { ok: false, code: 'TOO_FEW_VERTICES', message: '정점이 3개 미만입니다.' }
+ }
+ if (outerline.length > MAX_VERTICES) {
+ return { ok: false, code: 'TOO_MANY_VERTICES', message: `정점이 ${MAX_VERTICES} 개를 초과합니다.` }
+ }
+ for (const p of outerline) {
+ if (typeof p?.x !== 'number' || typeof p?.y !== 'number' || !Number.isFinite(p.x) || !Number.isFinite(p.y)) {
+ return { ok: false, code: 'INVALID_VERTEX', message: '정점에 숫자가 아닌 좌표가 포함되어 있습니다.' }
+ }
+ }
+ // 면적 0 거부 (signed area)
+ let area = 0
+ for (let i = 0; i < outerline.length; i++) {
+ const cur = outerline[i]
+ const next = outerline[(i + 1) % outerline.length]
+ area += cur.x * next.y - next.x * cur.y
+ }
+ if (Math.abs(area) < 1) {
+ return { ok: false, code: 'ZERO_AREA', message: '폴리곤 면적이 0 입니다.' }
+ }
+ return { ok: true, empty: false }
+}
+
+const waitForFileActive = async (fileManager, fileName, { timeoutMs = 60000, intervalMs = 1500 } = {}) => {
+ const started = Date.now()
+ let file = await fileManager.getFile(fileName)
+ while (file.state === FileState.PROCESSING) {
+ if (Date.now() - started > timeoutMs) {
+ throw new Error('Gemini 파일 처리 타임아웃')
+ }
+ await new Promise((r) => setTimeout(r, intervalMs))
+ file = await fileManager.getFile(fileName)
+ }
+ if (file.state !== FileState.ACTIVE) {
+ throw new Error(`Gemini 파일 처리 실패: ${file.state}`)
+ }
+ return file
+}
+
+export async function POST(req) {
+ const apiKey = process.env.GEMINI_API_KEY
+ const modelName = process.env.GEMINI_MODEL || DEFAULT_MODEL
+
+ if (!apiKey) {
+ return NextResponse.json({ error: { code: 'NO_API_KEY', message: 'GEMINI_API_KEY 가 설정되지 않았습니다.' } }, { status: 500 })
+ }
+
+ let uploadedFileName = null
+ const fileManager = new GoogleAIFileManager(apiKey)
+
+ try {
+ const formData = await req.formData()
+ const file = formData.get('file')
+
+ if (!file || typeof file === 'string') {
+ return NextResponse.json({ error: { code: 'NO_FILE', message: '파일이 전달되지 않았습니다.' } }, { status: 400 })
+ }
+ if (file.type !== 'application/pdf') {
+ return NextResponse.json({ error: { code: 'INVALID_MIME', message: 'PDF 파일만 업로드할 수 있습니다.' } }, { status: 400 })
+ }
+ if (file.size > MAX_FILE_BYTES) {
+ return NextResponse.json(
+ { error: { code: 'FILE_TOO_LARGE', message: `파일 크기가 ${Math.round(MAX_FILE_BYTES / 1024 / 1024)}MB 를 초과합니다.` } },
+ { status: 400 },
+ )
+ }
+
+ const pageMode = formData.get('pageMode') || 'all'
+ const facadePages = formData.get('facadePages') || ''
+ const floorPages = formData.get('floorPages') || ''
+
+ const buffer = Buffer.from(await file.arrayBuffer())
+
+ const uploadResult = await fileManager.uploadFile(buffer, {
+ mimeType: 'application/pdf',
+ displayName: file.name || 'floor-plan.pdf',
+ })
+ uploadedFileName = uploadResult.file.name
+
+ await waitForFileActive(fileManager, uploadedFileName)
+
+ const genAI = new GoogleGenerativeAI(apiKey)
+ const model = genAI.getGenerativeModel({
+ model: modelName,
+ generationConfig: {
+ responseMimeType: 'application/json',
+ responseSchema: FLOOR_PLAN_SCHEMA,
+ temperature: 0.1,
+ },
+ })
+
+ const prompt = buildPrompt({ pageMode, facadePages, floorPages })
+
+ const result = await model.generateContent([
+ {
+ fileData: {
+ fileUri: uploadResult.file.uri,
+ mimeType: uploadResult.file.mimeType,
+ },
+ },
+ { text: prompt },
+ ])
+
+ const raw = result.response.text()
+ logger.debug('[gemini/floor-plan] raw response', raw)
+
+ let parsed
+ try {
+ parsed = JSON.parse(raw)
+ } catch (e) {
+ return NextResponse.json({ error: { code: 'PARSE_FAILED', message: '응답 JSON 파싱 실패' } }, { status: 502 })
+ }
+
+ const validation = validateOuterline(parsed.outerline)
+ if (!validation.ok) {
+ return NextResponse.json({ error: { code: validation.code, message: validation.message } }, { status: 422 })
+ }
+
+ return NextResponse.json({
+ outerline: parsed.outerline,
+ unit: parsed.unit || 'mm',
+ scale: parsed.scale ?? null,
+ confidence: parsed.confidence ?? null,
+ notes: parsed.notes ?? null,
+ empty: validation.empty,
+ })
+ } catch (error) {
+ logger.error('[gemini/floor-plan] error', error?.message || error)
+ return NextResponse.json({ error: { code: 'GEMINI_FAILED', message: error?.message || 'Gemini 호출에 실패했습니다.' } }, { status: 500 })
+ } finally {
+ if (uploadedFileName) {
+ try {
+ await fileManager.deleteFile(uploadedFileName)
+ } catch (deleteError) {
+ logger.warn('[gemini/floor-plan] deleteFile failed', deleteError?.message || deleteError)
+ }
+ }
+ }
+}
diff --git a/src/components/floor-plan/modal/placementShape/PlacementShapeSetting.jsx b/src/components/floor-plan/modal/placementShape/PlacementShapeSetting.jsx
index ee75ea20..fc4f9e08 100644
--- a/src/components/floor-plan/modal/placementShape/PlacementShapeSetting.jsx
+++ b/src/components/floor-plan/modal/placementShape/PlacementShapeSetting.jsx
@@ -23,12 +23,27 @@ import { useRoofFn } from '@/hooks/common/useRoofFn'
import { usePlan } from '@/hooks/usePlan'
import { normalizeDecimal } from '@/util/input-utils'
import { CalculatorInput } from '@/components/common/input/CalcInput'
+import { usePdfImport } from '@/hooks/pdf-import/usePdfImport'
+import { logger } from '@/util/logger'
// [LOW-PITCH-WARN 2026-05-06] 저구배 + 특정 기와 사용 시 시공 매뉴얼 안내 alert
import { useSwal } from '@/hooks/useSwal'
import { notifyLowPitchRestrictionForRoofs, isLowPitchRestricted, LOW_PITCH_RESTRICTED_ROOF_IDS } from '@/util/roof-pitch-warning'
// [LOW-PITCH-DIAG 2026-05-06 TEMP] 진단용 — 검증 후 import + 호출 함께 제거
import { debugCapture } from '@/util/debugCapture'
+const INPUT_MODE = {
+ SIZE_ROOF: '1',
+ SIZE_ACTUAL: '2',
+ PDF: 'pdf',
+}
+
+const PDF_MAX_FILE_BYTES = 20 * 1024 * 1024
+
+const PDF_PAGE_MODE = {
+ ALL: 'all',
+ SPECIFY: 'specify',
+}
+
/**
* 지붕 레이아웃
*/
@@ -76,15 +91,112 @@ export default function PlacementShapeSetting({ id, pos = { x: 50, y: 180 }, pla
const { saveCanvas } = usePlan()
// [LOW-PITCH-WARN 2026-05-06]
const { swalFire } = useSwal()
+ const { applyOuterline } = usePdfImport()
+
+ // PDF 업로드 관련 상태
+ const [inputMode, setInputMode] = useState(INPUT_MODE.SIZE_ROOF)
+ const [pdfFile, setPdfFile] = useState(null)
+ const [pdfPageMode, setPdfPageMode] = useState(PDF_PAGE_MODE.ALL)
+ const [pdfFacadePages, setPdfFacadePages] = useState('')
+ const [pdfFloorPages, setPdfFloorPages] = useState('')
+ const [pdfAnalyzing, setPdfAnalyzing] = useState(false)
+ const pdfInputRef = useRef(null)
/**
- * 치수 입력방법(복시도입력/실측값입력/육지붕)
+ * 치수 입력방법(복시도입력/실측값입력/도면 파일 업로드)
*/
const roofSizeSetArray = [
- { id: 'ra01', name: 'roofSizeSet', value: '1', message: 'modal.placement.initial.setting.size.roof' },
- { id: 'ra02', name: 'roofSizeSet', value: '2', message: 'modal.placement.initial.setting.size.actual' },
- // { id: 'ra03', name: 'roofSizeSet', value: '3', message: 'modal.placement.initial.setting.size.none.pitch' },
+ { id: 'ra01', name: 'roofSizeSet', value: INPUT_MODE.SIZE_ROOF, message: 'modal.placement.initial.setting.size.roof' },
+ { id: 'ra02', name: 'roofSizeSet', value: INPUT_MODE.SIZE_ACTUAL, message: 'modal.placement.initial.setting.size.actual' },
+ { id: 'ra-pdf', name: 'roofSizeSet', value: INPUT_MODE.PDF, message: 'modal.placement.initial.setting.size.pdf' },
]
+ const handleInputModeChange = (value) => {
+ setInputMode(value)
+ if (value === INPUT_MODE.PDF) {
+ // PDF 모드는 내부적으로 복시도(1) 로 저장된다.
+ setCurrentRoof((prev) => ({ ...prev, roofSizeSet: INPUT_MODE.SIZE_ROOF }))
+ } else {
+ setCurrentRoof((prev) => ({ ...prev, roofSizeSet: value }))
+ }
+ }
+
+ const resetPdfFile = () => {
+ setPdfFile(null)
+ if (pdfInputRef.current) pdfInputRef.current.value = ''
+ }
+
+ const handlePdfFileChange = (e) => {
+ const next = e.target.files?.[0]
+ if (!next) return
+ if (next.type !== 'application/pdf') {
+ swalFire({ text: getMessage('modal.placement.initial.setting.size.pdf.error.mime'), type: 'alert' })
+ e.target.value = ''
+ return
+ }
+ if (next.size > PDF_MAX_FILE_BYTES) {
+ swalFire({ text: getMessage('modal.placement.initial.setting.size.pdf.error.too.large'), type: 'alert' })
+ e.target.value = ''
+ return
+ }
+ setPdfFile(next)
+ }
+
+ const analyzePdfAndApply = async () => {
+ if (!pdfFile) {
+ swalFire({ text: getMessage('modal.placement.initial.setting.size.pdf.error.no.file'), type: 'alert' })
+ return false
+ }
+
+ const formData = new FormData()
+ formData.append('file', pdfFile)
+ formData.append('unitHint', 'mm')
+ formData.append('pageMode', pdfPageMode)
+ if (pdfPageMode === PDF_PAGE_MODE.SPECIFY) {
+ formData.append('facadePages', pdfFacadePages.trim())
+ formData.append('floorPages', pdfFloorPages.trim())
+ }
+
+ setPdfAnalyzing(true)
+ let response
+ try {
+ response = await fetch('/api/gemini/floor-plan', { method: 'POST', body: formData })
+ } catch (e) {
+ logger.error('[PlacementShapeSetting] PDF analyze fetch failed', e?.message || e)
+ setPdfAnalyzing(false)
+ swalFire({ text: getMessage('pdf.import.error.network'), type: 'alert' })
+ return false
+ }
+
+ let payload
+ try {
+ payload = await response.json()
+ } catch (e) {
+ logger.error('[PlacementShapeSetting] PDF analyze response parse failed', e?.message || e)
+ setPdfAnalyzing(false)
+ swalFire({ text: getMessage('pdf.import.error.analyze'), type: 'alert' })
+ return false
+ }
+
+ setPdfAnalyzing(false)
+
+ if (!response.ok) {
+ const msg = payload?.error?.message || getMessage('pdf.import.error.analyze')
+ swalFire({ text: msg, type: 'alert' })
+ return false
+ }
+ if (payload.empty || !Array.isArray(payload.outerline) || payload.outerline.length === 0) {
+ swalFire({ text: payload.notes || getMessage('pdf.import.error.no.floorplan'), type: 'alert' })
+ return false
+ }
+
+ const applied = await applyOuterline(payload.outerline, { unit: payload.unit || 'mm' })
+ if (!applied) {
+ swalFire({ text: getMessage('pdf.import.error.apply'), type: 'alert' })
+ return false
+ }
+ return true
+ }
+
/**
* 지붕각도 설정(경사/각도)
*/
@@ -138,7 +250,7 @@ export default function PlacementShapeSetting({ id, pos = { x: 50, y: 180 }, pla
*/
useEffect(() => {
if (currentCanvasPlan?.planNo && currentRoof) {
- setCurrentRoof(prev => ({ ...prev, planNo: currentCanvasPlan.planNo }))
+ setCurrentRoof((prev) => ({ ...prev, planNo: currentCanvasPlan.planNo }))
}
}, [currentCanvasPlan?.planNo])
@@ -238,6 +350,13 @@ export default function PlacementShapeSetting({ id, pos = { x: 50, y: 180 }, pla
* 배치면초기설정 저장 버튼 클릭
*/
const handleSaveBtn = async () => {
+ if (pdfAnalyzing) return
+
+ if (inputMode === INPUT_MODE.PDF) {
+ const ok = await analyzePdfAndApply()
+ if (!ok) return
+ }
+
const roofInfo = {
...currentRoof,
planNo: currentCanvasPlan?.planNo || basicSetting.planNo,
@@ -273,16 +392,19 @@ export default function PlacementShapeSetting({ id, pos = { x: 50, y: 180 }, pla
/**
* 배치면초기설정 저장 (메뉴 변경/useEffect 트리거 없이)
*/
- basicSettingSave({
- ...basicSetting,
- planNo: currentCanvasPlan?.planNo || basicSetting.planNo,
- /**
- * 선택된 지붕재 정보
- */
- selectedRoofMaterial: {
- ...newAddedRoofs[0],
+ basicSettingSave(
+ {
+ ...basicSetting,
+ planNo: currentCanvasPlan?.planNo || basicSetting.planNo,
+ /**
+ * 선택된 지붕재 정보
+ */
+ selectedRoofMaterial: {
+ ...newAddedRoofs[0],
+ },
},
- }, { skipSideEffects: true })
+ { skipSideEffects: true },
+ )
const roofs = canvas.getObjects().filter((obj) => obj.roofMaterial?.index === 0)
@@ -297,7 +419,7 @@ export default function PlacementShapeSetting({ id, pos = { x: 50, y: 180 }, pla
/** 지붕면 존재 여부에 따라 메뉴 설정 */
const hasRoofs = canvas.getObjects().some((obj) => obj.name === POLYGON_TYPE.ROOF)
-
+
// roofSizeSet에 따라 메뉴 설정
if (currentRoof?.roofSizeSet === '2') {
setSelectedMenu('surface')
@@ -354,15 +476,119 @@ export default function PlacementShapeSetting({ id, pos = { x: 50, y: 180 }, pla
id={item.id}
name={item.name}
value={item.value}
- checked={String(currentRoof?.roofSizeSet) === item.value}
- onChange={(e) => setCurrentRoof({ ...currentRoof, roofSizeSet: e.target.value })}
+ checked={inputMode === item.value}
+ onChange={(e) => handleInputModeChange(e.target.value)}
/>
{getMessage(item.message)}
))}
+ {inputMode === INPUT_MODE.PDF && (
+
+ {getMessage('modal.placement.initial.setting.size.pdf.info')}
+
+ )}
+ {inputMode === INPUT_MODE.PDF && (
+ <>
+
+ {getMessage('modal.placement.initial.setting.size.pdf.file.label')}
+
+
+
+ {getMessage('modal.placement.initial.setting.size.pdf.file.info')}
+
+
+
+
+ {getMessage('modal.placement.initial.setting.size.pdf.page.label')}
+
+
+
+ {pdfPageMode === PDF_PAGE_MODE.SPECIFY
+ ? getMessage('modal.placement.initial.setting.size.pdf.page.specify.info')
+ : getMessage('modal.placement.initial.setting.size.pdf.page.all.info')}
+
+
+
+ >
+ )}
@@ -630,10 +856,19 @@ export default function PlacementShapeSetting({ id, pos = { x: 50, y: 180 }, pla
setUseCalcPad((prev) => !prev)}>
-
+
-
- {getMessage('modal.common.save')}
+
+ {pdfAnalyzing ? getMessage('pdf.import.button.analyzing') : getMessage('modal.common.save')}
diff --git a/src/hooks/pdf-import/usePdfImport.js b/src/hooks/pdf-import/usePdfImport.js
new file mode 100644
index 00000000..01fa4fb2
--- /dev/null
+++ b/src/hooks/pdf-import/usePdfImport.js
@@ -0,0 +1,176 @@
+import { useSetRecoilState, useRecoilValue } from 'recoil'
+import { canvasState } from '@/store/canvasAtom'
+import { outerLinePointsState } from '@/store/outerLineAtom'
+import { POLYGON_TYPE } from '@/common/common'
+import { QLine } from '@/components/fabric/QLine'
+import { usePolygon } from '@/hooks/usePolygon'
+import { useSwal } from '@/hooks/useSwal'
+import { useMessage } from '@/hooks/useMessage'
+import { logger } from '@/util/logger'
+
+const MM_PER_CANVAS_UNIT = 10
+const MAX_DIM = 1500
+const MIN_CANVAS_UNIT_LENGTH = 0.5
+
+const toCanvasUnits = (mmPoints) => mmPoints.map((p) => ({ x: p.x / MM_PER_CANVAS_UNIT, y: p.y / MM_PER_CANVAS_UNIT }))
+
+const polygonArea = (points) => {
+ let area = 0
+ for (let i = 0; i < points.length; i++) {
+ const cur = points[i]
+ const next = points[(i + 1) % points.length]
+ area += cur.x * next.y - next.x * cur.y
+ }
+ return area
+}
+
+const ensureCounterClockwise = (points) => {
+ // canvas 좌표계 (y 가 아래로 증가) 에서 CCW 는 signed area 가 음수가 된다.
+ return polygonArea(points) > 0 ? [...points].reverse() : points
+}
+
+const fitAndCenter = (points, canvas) => {
+ const xs = points.map((p) => p.x)
+ const ys = points.map((p) => p.y)
+ const minX = Math.min(...xs)
+ const minY = Math.min(...ys)
+ const maxX = Math.max(...xs)
+ const maxY = Math.max(...ys)
+ const width = maxX - minX
+ const height = maxY - minY
+
+ const targetWidth = (canvas?.getWidth?.() ?? 1000) * 0.6
+ const targetHeight = (canvas?.getHeight?.() ?? 1000) * 0.6
+ const maxAllowed = Math.min(MAX_DIM, Math.max(targetWidth, targetHeight))
+
+ let scale = 1
+ if (width > maxAllowed || height > maxAllowed) {
+ scale = Math.min(maxAllowed / width, maxAllowed / height)
+ }
+
+ const cx = (canvas?.getWidth?.() ?? 1000) / 2
+ const cy = (canvas?.getHeight?.() ?? 1000) / 2
+
+ return points.map((p) => ({
+ x: cx + (p.x - (minX + width / 2)) * scale,
+ y: cy + (p.y - (minY + height / 2)) * scale,
+ }))
+}
+
+export function usePdfImport() {
+ const canvas = useRecoilValue(canvasState)
+ const setOuterLinePoints = useSetRecoilState(outerLinePointsState)
+ const { addPolygonByLines } = usePolygon()
+ const { swalFire } = useSwal()
+ const { getMessage } = useMessage()
+
+ const hasExistingOuterLine = () => {
+ if (!canvas) return false
+ return canvas.getObjects().some((obj) => obj.name === 'outerLine' || obj.name === POLYGON_TYPE.WALL || obj.name === POLYGON_TYPE.ROOF)
+ }
+
+ const clearExistingCanvasState = () => {
+ if (!canvas) return
+ const removable = canvas.getObjects().filter((obj) => {
+ return (
+ obj.name === 'outerLine' ||
+ obj.name === 'outerLinePoint' ||
+ obj.name === 'startPoint' ||
+ obj.name === 'helpGuideLine' ||
+ obj.name === 'lengthText' ||
+ obj.name === 'pitchText' ||
+ obj.name === POLYGON_TYPE.WALL ||
+ obj.name === POLYGON_TYPE.ROOF
+ )
+ })
+ removable.forEach((obj) => canvas.remove(obj))
+ }
+
+ const drawOuterLines = (points) => {
+ points.forEach((point, idx) => {
+ if (idx === 0) return
+ const prev = points[idx - 1]
+ const line = new QLine([prev.x, prev.y, point.x, point.y], {
+ stroke: '#000000',
+ strokeWidth: 3,
+ idx,
+ selectable: true,
+ name: 'outerLine',
+ })
+ canvas.add(line)
+ })
+ }
+
+ const applyOuterline = async (mmPoints, options = {}) => {
+ if (!canvas) {
+ logger.warn('[usePdfImport] canvas 가 준비되지 않았습니다.')
+ return false
+ }
+
+ const { unit = 'mm' } = options
+ if (unit !== 'mm') {
+ logger.warn('[usePdfImport] 지원하지 않는 단위:', unit)
+ swalFire({ text: getMessage('pdf.import.error.unsupported.unit'), type: 'alert' })
+ return false
+ }
+
+ if (!Array.isArray(mmPoints) || mmPoints.length < 3) {
+ swalFire({ text: getMessage('pdf.import.error.too.few.vertices'), type: 'alert' })
+ return false
+ }
+
+ let canvasPoints = toCanvasUnits(mmPoints)
+ canvasPoints = canvasPoints.filter((p, idx, arr) => {
+ if (idx === 0) return true
+ const prev = arr[idx - 1]
+ const dx = p.x - prev.x
+ const dy = p.y - prev.y
+ return Math.hypot(dx, dy) >= MIN_CANVAS_UNIT_LENGTH
+ })
+
+ if (canvasPoints.length < 3) {
+ swalFire({ text: getMessage('pdf.import.error.too.few.vertices'), type: 'alert' })
+ return false
+ }
+
+ canvasPoints = fitAndCenter(canvasPoints, canvas)
+ canvasPoints = ensureCounterClockwise(canvasPoints)
+
+ const proceed = async () => {
+ clearExistingCanvasState()
+ const closedPoints = [...canvasPoints, canvasPoints[0]]
+ setOuterLinePoints(closedPoints)
+ drawOuterLines(closedPoints)
+
+ try {
+ const lines = canvas.getObjects().filter((obj) => obj.name === 'outerLine')
+ if (lines.length >= 3) {
+ addPolygonByLines(lines, { name: POLYGON_TYPE.WALL, fill: 'transparent', stroke: 'black' })
+ }
+ } catch (e) {
+ logger.warn('[usePdfImport] wall polygon 생성 실패', e?.message || e)
+ }
+
+ canvas.renderAll()
+ }
+
+ if (hasExistingOuterLine()) {
+ return await new Promise((resolve) => {
+ swalFire({
+ text: getMessage('pdf.import.confirm.overwrite'),
+ type: 'confirm',
+ confirmFn: async () => {
+ await proceed()
+ resolve(true)
+ },
+ denyFn: () => resolve(false),
+ })
+ })
+ }
+
+ await proceed()
+ return true
+ }
+
+ return { applyOuterline, hasExistingOuterLine }
+}
diff --git a/src/locales/ja.json b/src/locales/ja.json
index ba535999..88e88a2a 100644
--- a/src/locales/ja.json
+++ b/src/locales/ja.json
@@ -16,6 +16,33 @@
"header.stem": "Stem",
"plan.menu.plan.drawing": "物件情報",
"plan.menu.placement.surface.initial.setting": "配置面初期設定",
+ "pdf.import.button.analyzing": "解析中...",
+ "pdf.import.success": "外壁線をキャンバスに反映しました。",
+ "pdf.import.confirm.overwrite": "既存の外壁線があります。新しい外壁線で上書きしますか?",
+ "pdf.import.error.network": "ネットワークエラーで解析リクエストに失敗しました。",
+ "pdf.import.error.analyze": "PDF 解析に失敗しました。",
+ "pdf.import.error.no.floorplan": "平面図を認識できませんでした。別の PDF で再度お試しください。",
+ "pdf.import.error.too.few.vertices": "外壁線の頂点が不足しています。別の PDF で再度お試しください。",
+ "pdf.import.error.unsupported.unit": "未対応の座標単位です。",
+ "pdf.import.error.apply": "解析結果をキャンバスに反映できませんでした。",
+ "modal.placement.initial.setting.size.pdf": "図面ファイルアップロード",
+ "modal.placement.initial.setting.size.pdf.info": "住宅図面ファイルをアップロード後、全ページ解析または立面図·平面図ページ指定方式で外壁線と屋根形状を作成できます。\n住宅図面ファイルアップロードによる自動図面作成は、必ずしも正確な結果を保証しません。\n見積作成後、必ず住宅図面と出力結果が一致するか確認してください。",
+ "modal.placement.initial.setting.size.pdf.file.label": "住宅図面ファイルアップロード",
+ "modal.placement.initial.setting.size.pdf.file.upload": "アップロード",
+ "modal.placement.initial.setting.size.pdf.file.placeholder": "ファイルをドラッグまたはクリックでアップロード (.PDF)",
+ "modal.placement.initial.setting.size.pdf.file.info": "住宅図面の解析結果に応じて外壁線と屋根形状が一緒に生成されます。屋根形状を認識できない場合は外壁線のみ生成され、屋根面は手動で作成します。",
+ "modal.placement.initial.setting.size.pdf.page.label": "読み込みページ設定",
+ "modal.placement.initial.setting.size.pdf.page.all": "全ページ",
+ "modal.placement.initial.setting.size.pdf.page.specify": "ページ指定",
+ "modal.placement.initial.setting.size.pdf.page.facade.label": "立面図",
+ "modal.placement.initial.setting.size.pdf.page.floor.label": "平面図",
+ "modal.placement.initial.setting.size.pdf.page.example.facade": "例) 1, 2",
+ "modal.placement.initial.setting.size.pdf.page.example.floor": "例) 3, 4",
+ "modal.placement.initial.setting.size.pdf.page.all.info": "全ページを解析するため処理時間が長くなる場合があります。図面内の立面図·平面図ページが特定できる場合はページ指定を推奨します。",
+ "modal.placement.initial.setting.size.pdf.page.specify.info": "立面図·平面図が含まれるページ番号をそれぞれ入力してください。指定したページのみ読み込み素早く屋根を自動作成します。",
+ "modal.placement.initial.setting.size.pdf.error.mime": "PDF ファイルのみアップロード可能です。",
+ "modal.placement.initial.setting.size.pdf.error.too.large": "ファイルサイズが 20MB を超えています。",
+ "modal.placement.initial.setting.size.pdf.error.no.file": "先に PDF ファイルをアップロードしてください。",
"modal.placement.initial.setting.plan.drawing": "図面の作成方法",
"modal.placement.initial.setting.plan.drawing.size.stuff": "寸法入力による物件作成",
"modal.placement.initial.setting.plan.drawing.size.info": "※数字は[半角]入力のみ可能です。",
diff --git a/src/locales/ko.json b/src/locales/ko.json
index a596bffe..956d552d 100644
--- a/src/locales/ko.json
+++ b/src/locales/ko.json
@@ -16,6 +16,33 @@
"header.stem": "Stem",
"plan.menu.plan.drawing": "물건정보",
"plan.menu.placement.surface.initial.setting": "배치면 초기설정",
+ "pdf.import.button.analyzing": "분석 중...",
+ "pdf.import.success": "외곽선이 캔버스에 반영되었습니다.",
+ "pdf.import.confirm.overwrite": "기존 외곽선이 있습니다. 새 외곽선으로 덮어쓰시겠습니까?",
+ "pdf.import.error.network": "네트워크 오류로 분석 요청에 실패했습니다.",
+ "pdf.import.error.analyze": "PDF 분석에 실패했습니다.",
+ "pdf.import.error.no.floorplan": "평면도를 인식하지 못했습니다. 다른 PDF 로 다시 시도해 주세요.",
+ "pdf.import.error.too.few.vertices": "외곽선 정점이 부족합니다. 다른 PDF 로 다시 시도해 주세요.",
+ "pdf.import.error.unsupported.unit": "지원하지 않는 좌표 단위입니다.",
+ "pdf.import.error.apply": "분석 결과를 캔버스에 반영하지 못했습니다.",
+ "modal.placement.initial.setting.size.pdf": "도면 파일 업로드",
+ "modal.placement.initial.setting.size.pdf.info": "주택 도면 파일 업로드 후 전체 페이지 분석 또는 입면도·평면도 페이지 지정 방식으로 외벽선과 지붕 형상을 작성할 수 있습니다.\n주택 도면 파일 업로드를 통한 자동 도면 작성은 반드시 정확한 결과라고 보장할 수 없습니다.\n견적 작성 후 반드시 주택 도면과 출력 결과가 일치하는지 확인해 주십시오.",
+ "modal.placement.initial.setting.size.pdf.file.label": "주택 도면 파일 업로드",
+ "modal.placement.initial.setting.size.pdf.file.upload": "업로드",
+ "modal.placement.initial.setting.size.pdf.file.placeholder": "파일을 드래그하거나 클릭하여 업로드 (.PDF)",
+ "modal.placement.initial.setting.size.pdf.file.info": "주택 도면 분석 결과에 따라 외벽선과 지붕형상이 함께 생성됩니다. 지붕형상을 인식하지 못한 경우 외벽선만 생성되며, 지붕면은 수동으로 작성합니다.",
+ "modal.placement.initial.setting.size.pdf.page.label": "읽기 페이지 설정",
+ "modal.placement.initial.setting.size.pdf.page.all": "전체 페이지",
+ "modal.placement.initial.setting.size.pdf.page.specify": "페이지 지정",
+ "modal.placement.initial.setting.size.pdf.page.facade.label": "입면도",
+ "modal.placement.initial.setting.size.pdf.page.floor.label": "평면도",
+ "modal.placement.initial.setting.size.pdf.page.example.facade": "예) 1, 2",
+ "modal.placement.initial.setting.size.pdf.page.example.floor": "예) 3, 4",
+ "modal.placement.initial.setting.size.pdf.page.all.info": "모든 페이지를 분석하므로 처리 시간이 길어질 수 있습니다. 도면 내 입면도·평면도 페이지가 확인되면 페이지 지정을 권장합니다.",
+ "modal.placement.initial.setting.size.pdf.page.specify.info": "입면도·평면도가 포함된 페이지 번호를 각각 입력해주세요. 지정한 페이지만 읽어 빠르게 지붕을 자동 작성합니다.",
+ "modal.placement.initial.setting.size.pdf.error.mime": "PDF 파일만 업로드할 수 있습니다.",
+ "modal.placement.initial.setting.size.pdf.error.too.large": "파일 크기가 20MB 를 초과합니다.",
+ "modal.placement.initial.setting.size.pdf.error.no.file": "PDF 파일을 먼저 업로드해 주세요.",
"modal.placement.initial.setting.plan.drawing": "도면 작성방법",
"modal.placement.initial.setting.plan.drawing.size.stuff": "치수 입력에 의한 물건 작성",
"modal.placement.initial.setting.plan.drawing.size.info": "※숫자는 [반각] 입력만 가능합니다.",
diff --git a/src/styles/_contents.scss b/src/styles/_contents.scss
index 58f9a3ca..14c24fbc 100644
--- a/src/styles/_contents.scss
+++ b/src/styles/_contents.scss
@@ -17,1539 +17,1593 @@
// }
// }
// CanvasMenu
-.canvas-menu-wrap{
- position: fixed;
- top: 46px;
- left: 0;
- display: block;
- width: 100%;
- min-width: 1280px;
- padding-bottom: 0;
- background-color: #383838;
- transition: padding .17s ease-in-out;
+.canvas-menu-wrap {
+ position: fixed;
+ top: 46px;
+ left: 0;
+ display: block;
+ width: 100%;
+ min-width: 1280px;
+ padding-bottom: 0;
+ background-color: #383838;
+ transition: padding 0.17s ease-in-out;
+ z-index: 999;
+ .canvas-menu-inner {
+ position: relative;
+ display: flex;
+ align-items: center;
+ padding: 0 40px 0 20px;
+ background-color: #2c2c2c;
+ height: 46.8px;
z-index: 999;
- .canvas-menu-inner{
- position: relative;
+ .canvas-menu-list {
+ display: flex;
+ align-items: center;
+ height: 100%;
+ .canvas-menu-item {
display: flex;
align-items: center;
- padding: 0 40px 0 20px;
- background-color: #2C2C2C;
- height: 46.8px;
- z-index: 999;
- .canvas-menu-list{
- display: flex;
- align-items: center;
- height: 100%;
- .canvas-menu-item{
- display: flex;
- align-items: center;
- height: 100%;
- button{
- display: flex;
- align-items: center;
- font-size: 12px;
- height: 100%;
- color: #fff;
- font-weight: 600;
- padding: 15px 20px;
- opacity: 0.55;
- transition: all .17s ease-in-out;
- .menu-icon{
- display: block;
- width: 14px;
- height: 14px;
- background-repeat: no-repeat;
- background-position: center;
- background-size: contain;
- margin-right: 10px;
- &.con00{background-image: url(/static/images/canvas/menu_icon00.svg);}
- &.con01{background-image: url(/static/images/canvas/menu_icon01.svg);}
- &.con02{background-image: url(/static/images/canvas/menu_icon02.svg);}
- &.con03{background-image: url(/static/images/canvas/menu_icon03.svg);}
- &.con04{background-image: url(/static/images/canvas/menu_icon04.svg);}
- &.con05{background-image: url(/static/images/canvas/menu_icon05.svg);}
- &.con06{background-image: url(/static/images/canvas/menu_icon06.svg);}
- }
- }
- &.active{
- background-color: #383838;
- button{
- opacity: 1;
- }
- }
+ height: 100%;
+ button {
+ display: flex;
+ align-items: center;
+ font-size: 12px;
+ height: 100%;
+ color: #fff;
+ font-weight: 600;
+ padding: 15px 20px;
+ opacity: 0.55;
+ transition: all 0.17s ease-in-out;
+ .menu-icon {
+ display: block;
+ width: 14px;
+ height: 14px;
+ background-repeat: no-repeat;
+ background-position: center;
+ background-size: contain;
+ margin-right: 10px;
+ &.con00 {
+ background-image: url(/static/images/canvas/menu_icon00.svg);
}
+ &.con01 {
+ background-image: url(/static/images/canvas/menu_icon01.svg);
+ }
+ &.con02 {
+ background-image: url(/static/images/canvas/menu_icon02.svg);
+ }
+ &.con03 {
+ background-image: url(/static/images/canvas/menu_icon03.svg);
+ }
+ &.con04 {
+ background-image: url(/static/images/canvas/menu_icon04.svg);
+ }
+ &.con05 {
+ background-image: url(/static/images/canvas/menu_icon05.svg);
+ }
+ &.con06 {
+ background-image: url(/static/images/canvas/menu_icon06.svg);
+ }
+ }
}
- .canvas-side-btn-wrap{
- display: flex;
- align-items: center;
- margin-left: auto;
- .select-box{
- width: 124px;
- margin: 0 5px;
- height: 30px;
- > div{
- width: 100%;
- }
- }
- .btn-from{
- display: flex;
- align-items: center;
- gap: 5px;
- button{
- display: block;
- width: 30px;
- height: 30px;
- border-radius: 2px;
- background-color: #3D3D3D;
- background-position: center;
- background-repeat: no-repeat;
- background-size: 15px 15px;
- transition: all .17s ease-in-out;
- &.btn01{background-image: url(../../public/static/images/canvas/side_icon03.svg);}
- &.btn02{background-image: url(../../public/static/images/canvas/side_icon02.svg);}
- &.btn03{background-image: url(../../public/static/images/canvas/side_icon01.svg);}
- &.btn04{background-image: url(../../public/static/images/canvas/side_icon04.svg);}
- &.btn05{background-image: url(../../public/static/images/canvas/side_icon05.svg);}
- &.btn06{background-image: url(../../public/static/images/canvas/side_icon06.svg);}
- &.btn07{background-image: url(../../public/static/images/canvas/side_icon07.svg);}
- &.btn08{background-image: url(../../public/static/images/canvas/side_icon08.svg);}
- &.btn09{background-image: url(../../public/static/images/canvas/side_icon09.svg);}
- &.btn10{background-image: url(../../public/static/images/canvas/side_icon10.svg); background-size: 15px 14px;}
- &:hover{
- background-color: #1083E3;
- }
- &.active{
- background-color: #1083E3;
- }
- }
- }
- .ico-btn-from{
- display: flex;
- align-items: center;
- gap: 5px;
- button{
- .ico{
- display: block;
- width: 15px;
- height: 15px;
- background-repeat: no-repeat;
- background-position: center;
- background-size: contain;
- &.ico01{background-image: url(../../public/static/images/canvas/ico-flx01.svg);}
- &.ico02{background-image: url(../../public/static/images/canvas/ico-flx02.svg);}
- &.ico03{background-image: url(../../public/static/images/canvas/ico-flx03.svg);}
- &.ico04{background-image: url(../../public/static/images/canvas/ico-flx04.svg);}
- &.ico05{background-image: url(../../public/static/images/canvas/ico-flx05.svg);}
- }
- .name{
- font-size: 12px;
- color: #fff;
- }
- }
- &.form06{
- .name{
- font-size: 13px;
- }
- }
- }
- .vertical-horizontal{
- display: flex;
- min-width: 170px;
- height: 28px;
- margin-right: 5px;
- border-radius: 2px;
- background: #373737;
- line-height: 28px;
- overflow: hidden;
- span{
- padding: 0 10px;
- font-size: 13px;
- color: #fff;
- }
- button{
- margin-left: auto;
- height: 100%;
- background-color: #4B4B4B;
- font-size: 13px;
- font-weight: 400;
- color: #fff;
- padding: 0 7.5px;
- transition: all .17s ease-in-out;
- }
- &.on{
- button{
- background-color: #1083E3;
- }
- }
- }
- .size-control{
- display: flex;
- align-items: center;
- justify-content: center;
- gap: 10px;
- background-color: #3D3D3D;
- border-radius: 2px;
- width: 100px;
- height: 30px;
- margin: 0 5px;
- span{
- font-size: 13px;
- color: #fff;
- cursor: pointer;
- }
- .control-btn{
- display: block;
- width: 12px;
- height: 12px;
- background-repeat: no-repeat;
- background-size: cover;
- background-position: center;
- &.minus{
- background-image: url(../../public/static/images/canvas/minus.svg);
- }
- &.plus{
- background-image: url(../../public/static/images/canvas/plus.svg);
- }
- }
- }
+ &.active {
+ background-color: #383838;
+ button {
+ opacity: 1;
+ }
}
+ }
}
- .canvas-depth2-wrap{
- position: absolute;
- top: -100%;
- left: 0;
- background-color: #383838;
- width: 100%;
- height: 50px;
- transition: all .17s ease-in-out;
- .canvas-depth2-inner{
- display: flex;
- align-items: center;
- padding: 0 40px;
- height: 100%;
- .canvas-depth2-list{
- display: flex;
- align-items: center ;
- height: 100%;
- .canvas-depth2-item{
- display: flex;
- align-items: center;
- margin-right: 26px;
- height: 100%;
- button{
- position: relative;
- opacity: 0.55;
- color: #fff;
- font-size: 12px;
- font-weight: normal;
- height: 100%;
- padding-right: 12px;
- }
- &.active{
- button{
- opacity: 1;
- font-weight: 600;
- &:after{
- content: '';
- position: absolute;
- top: 50%;
- right: 0;
- transform: translateY(-50%);
- width: 5px;
- height: 8px;
- background: url(../../public/static/images/canvas/depth2-arr.svg) no-repeat center;
- }
- }
- }
- }
- }
- .canvas-depth2-btn-list{
- display: flex;
- align-items: center;
- margin-left: auto;
- height: 100%;
- .depth2-btn-box{
- display: flex;
- align-items: center;
- margin-right: 34px;
- height: 100%;
- transition: all .17s ease-in-out;
- button{
- position: relative;
- font-size: 12px;
- font-weight: 400;
- height: 100%;
- color: #fff;
- padding-right: 12px;
- &:after{
- content: '';
- position: absolute;
- top: 50%;
- right: 0;
- transform: translateY(-50%);
- width: 5px;
- height: 8px;
- background: url(../../public/static/images/canvas/depth2-arr.svg) no-repeat center;
- }
- }
- &:last-child{
- margin-right: 0;
- }
- &.mouse{
- opacity: 0.55;
- }
- }
- }
+ .canvas-side-btn-wrap {
+ display: flex;
+ align-items: center;
+ margin-left: auto;
+ .select-box {
+ width: 124px;
+ margin: 0 5px;
+ height: 30px;
+ > div {
+ width: 100%;
}
- &.active{
- top: 47px;
- }
- }
- &.active{
- padding-bottom: 50px;
- }
-}
-
-// canvas-layout
-.canvas-content{
- padding-top: 46.8px;
- transition: all .17s ease-in-out;
- .canvas-frame{
- height: calc(100vh - 129.3px);
- }
- &.active{
- padding-top: calc(46.8px + 50px);
- .canvas-frame{
- height: calc(100vh - 179.4px);
- }
- }
-}
-.canvas-layout{
- padding-top: 37px;
- .canvas-page-list{
- position: fixed;
- top: 92.8px;
- left: 0;
+ }
+ .btn-from {
display: flex;
- background-color: #1C1C1C;
- border-top: 1px solid #000;
- width: 100%;
- min-width: 1280px;
- transition: all .17s ease-in-out;
- z-index: 99;
- &.active{
- top: calc(92.8px + 50px);
+ align-items: center;
+ gap: 5px;
+ button {
+ display: block;
+ width: 30px;
+ height: 30px;
+ border-radius: 2px;
+ background-color: #3d3d3d;
+ background-position: center;
+ background-repeat: no-repeat;
+ background-size: 15px 15px;
+ transition: all 0.17s ease-in-out;
+ &.btn01 {
+ background-image: url(../../public/static/images/canvas/side_icon03.svg);
+ }
+ &.btn02 {
+ background-image: url(../../public/static/images/canvas/side_icon02.svg);
+ }
+ &.btn03 {
+ background-image: url(../../public/static/images/canvas/side_icon01.svg);
+ }
+ &.btn04 {
+ background-image: url(../../public/static/images/canvas/side_icon04.svg);
+ }
+ &.btn05 {
+ background-image: url(../../public/static/images/canvas/side_icon05.svg);
+ }
+ &.btn06 {
+ background-image: url(../../public/static/images/canvas/side_icon06.svg);
+ }
+ &.btn07 {
+ background-image: url(../../public/static/images/canvas/side_icon07.svg);
+ }
+ &.btn08 {
+ background-image: url(../../public/static/images/canvas/side_icon08.svg);
+ }
+ &.btn09 {
+ background-image: url(../../public/static/images/canvas/side_icon09.svg);
+ }
+ &.btn10 {
+ background-image: url(../../public/static/images/canvas/side_icon10.svg);
+ background-size: 15px 14px;
+ }
+ &:hover {
+ background-color: #1083e3;
+ }
+ &.active {
+ background-color: #1083e3;
+ }
}
- .canvas-id{
- display: flex;
- align-items: center;
- padding: 9.6px 20px;
+ }
+ .ico-btn-from {
+ display: flex;
+ align-items: center;
+ gap: 5px;
+ button {
+ .ico {
+ display: block;
+ width: 15px;
+ height: 15px;
+ background-repeat: no-repeat;
+ background-position: center;
+ background-size: contain;
+ &.ico01 {
+ background-image: url(../../public/static/images/canvas/ico-flx01.svg);
+ }
+ &.ico02 {
+ background-image: url(../../public/static/images/canvas/ico-flx02.svg);
+ }
+ &.ico03 {
+ background-image: url(../../public/static/images/canvas/ico-flx03.svg);
+ }
+ &.ico04 {
+ background-image: url(../../public/static/images/canvas/ico-flx04.svg);
+ }
+ &.ico05 {
+ background-image: url(../../public/static/images/canvas/ico-flx05.svg);
+ }
+ }
+ .name {
font-size: 12px;
color: #fff;
- background-color: #1083E3;
+ }
}
- .canvas-plane-wrap{
- display: flex;
- align-items: center;
- max-width: calc(100% - 45px);
- .canvas-page-box{
- display: flex;
- align-items: center;
- background-color: #1c1c1c;
- padding: 9.6px 20px;
- border-right:1px solid #000;
- min-width: 0;
- transition: all .17s ease-in-out;
- span{
- display: flex;
- align-items: center;
- width: 100%;
- font-size: 12px;
- font-family: 'Pretendard', sans-serif;
- color: #AAA;
- white-space: nowrap;
- text-overflow: ellipsis;
- overflow: hidden;
- }
- .close{
- flex: none;
- display: block;
- width: 7px;
- height: 8px;
- margin-left: 15px;
- background: url(../../public/static/images/canvas/plan_close_gray.svg)no-repeat center;
- background-size: cover;
- }
- &.on{
- background-color: #fff;
- span{
- font-weight: 600;
- color: #101010;
- }
- .close{
- background: url(../../public/static/images/canvas/plan_close_black.svg)no-repeat center;
- }
- &:hover{
- background-color: #fff;
- }
- }
- &:hover{
- background-color: #000;
- }
- }
+ &.form06 {
+ .name {
+ font-size: 13px;
+ }
}
- .plane-add{
- display: flex;
- align-items: center;
- justify-content: center;
- width: 45px;
- padding: 13.5px 0;
- background-color: #1C1C1C;
- border-right: 1px solid #000;
- transition: all .17s ease-in-out;
- span{
- display: block;
- width: 9px;
- height: 9px;
- background: url(../../public/static/images/canvas/plane_add.svg)no-repeat center;
- background-size: cover;
- }
- &:hover{
- background-color: #000;
- }
- }
- }
-}
-
-.canvas-frame{
- position: relative;
- // height: calc(100% - 36.5px);
- background-color: #F4F4F7;
- overflow: auto;
- transition: all .17s ease-in-out;
- // &::-webkit-scrollbar {
- // width: 10px;
- // height: 10px;
- // background-color: #fff;
- // }
- // &::-webkit-scrollbar-thumb {
- // background-color: #C1CCD7;
- // border-radius: 30px;
- // }
- // &::-webkit-scrollbar-track {
- // background-color: #fff;
- // }
- .canvas-container{
- margin: 0 auto;
- background-color: #fff;
- }
- canvas{
- position: absolute;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- }
-}
-
-// sub-page
-.sub-header{
- position: fixed;
- top: 46px;
- left: 0;
- width: 100%;
- min-width: 1280px;
- height: 46px;
- border-bottom: 1px solid #000;
- background: #2C2C2C;
- z-index: 999;
- .sub-header-inner{
+ }
+ .vertical-horizontal {
display: flex;
- align-items: center;
- height: 100%;
- padding: 0 100px;
- .sub-header-title-wrap{
- display: flex;
- align-items: center;
- .title-item{
- position: relative;
- padding: 0 24px;
- a{
- display: flex;
- align-items: center;
- .icon{
- width: 22px;
- height: 22px;
- margin-right: 8px;
- background-repeat: no-repeat;
- background-position: center;
- background-size: cover;
- &.drawing{background-image: url(../../public/static/images/main/drawing_icon.svg);}
- }
- }
- &:after{
- content: '';
- position: absolute;
- top: 50%;
- right: 0;
- transform: translateY(-50%);
- width: 1px;
- height: 16px;
- background-color: #D9D9D9;
- }
- &:first-child{
- padding-left: 0;
- }
- &:last-child{
- padding-right: 0;
- &:after{
- display: none;
- }
- }
- }
+ min-width: 170px;
+ height: 28px;
+ margin-right: 5px;
+ border-radius: 2px;
+ background: #373737;
+ line-height: 28px;
+ overflow: hidden;
+ span {
+ padding: 0 10px;
+ font-size: 13px;
+ color: #fff;
}
- .sub-header-title{
- font-size: 16px;
- color: #fff;
- font-weight: 600;
+ button {
+ margin-left: auto;
+ height: 100%;
+ background-color: #4b4b4b;
+ font-size: 13px;
+ font-weight: 400;
+ color: #fff;
+ padding: 0 7.5px;
+ transition: all 0.17s ease-in-out;
}
- .sub-header-location{
- margin-left: auto;
- display: flex;
- align-items: center;
- .location-item{
- position: relative;
- display: flex;
- align-items: center;
- padding: 0 10px;
- span{
- display: flex;
- font-size: 12px;
- color: #AAA;
- font-weight: normal;
- cursor: default;
- }
- &:after{
- content: '';
- position: absolute;
- top: 50%;
- right: 0;
- transform: translateY(-50%);
- width: 4px;
- height: 6px;
- background: url(../../public/static/images/main/loaction_arr.svg)no-repeat center;
- }
- &:first-child{
- padding-left: 0;
- }
- &:last-child{
- padding-right: 0;
- span{
- color: #fff;
- }
- &:after{
- display: none;
- }
- }
- }
+ &.on {
+ button {
+ background-color: #1083e3;
+ }
}
- }
-}
-
-// sub content
-.sub-content{
- padding-top: 46px;
- .sub-content-inner{
- max-width: 1760px;
- margin: 0 auto;
- padding: 20px 20px 0;
- .sub-content-box{
- margin-bottom: 20px;
- &:last-child{
- margin-bottom: 0;
- }
- }
- }
- &.estimate{
- display: flex;
- flex-direction: column;
- padding-top: 0;
- .sub-content-inner{
- flex: 1;
- width: 100%;
- }
- }
-}
-.sub-table-box{
- padding: 20px;
- border-radius: 6px;
- border: 1px solid #E9EAED;
- background: #FFF;
- box-shadow: 0px 3px 30px 0px rgba(0, 0, 0, 0.02);
- .table-box-title-wrap{
- display: flex;
- align-items: center;
- margin-bottom: 15px;
- .title-wrap{
- display: flex;
- align-items: center;
- h3{
- display: block;
- font-size: 15px;
- color: #101010;
- font-weight: 600;
- margin-right: 14px;
- &.product{
- margin-right: 10px;
- }
- }
- .estimate-check-btn{
- position: relative;
- display: flex;
- align-items: center;
- padding-left: 10px;
- &::before{
- content: '';
- position: absolute;
- top: 50%;
- left: 0;
- transform: translateY(-50%);
- width: 1px;
- height: 11px;
- background-color: #D9D9D9;
- }
- }
- .product_tit{
- position: relative;
- font-size: 15px;
- font-weight: 600;
- color: #1083E3;
- padding-left: 10px;
- &::before{
- content: '';
- position: absolute;
- top: 50%;
- left: 0;
- transform: translateY(-50%);
- width: 1px;
- height: 11px;
- background-color: #D9D9D9;
- }
- }
- .option{
- padding-left: 5px;
- font-size: 13px;
- color: #101010;
- font-weight: 400;
- }
- .info-wrap{
- display: flex;
- align-items: center;
- li{
- position: relative;
- padding: 0 6px;
- font-size: 12px;
- color: #101010;
- font-weight: normal;
- span{
- font-weight: 600;
- &.red{
- color: #E23D70;
- }
- }
- &:after{
- content: '';
- position: absolute;
- top: 50%;
- right: 0;
- transform: translateY(-50%);
- width: 1px;
- height: 11px;
- background-color: #D9D9D9;
- }
- &:first-child{padding-left: 0;}
- &:last-child{padding-right: 0;&::after{display: none;}}
- }
- }
- }
- }
- .left-unit-box{
- margin-left: auto;
- display: flex;
- align-items: center;
- }
- .promise-title-wrap{
- display: flex;
- align-items: center;
- margin-bottom: 15px;
- .promise-gudie{
- margin-bottom: 0;
- }
- }
- .promise-gudie{
- display: block;
- font-size: 13px;
- font-weight: 700;
- color: #101010;
- margin-bottom: 20px;
- }
- .important{
- color: #f00;
- }
- .sub-center-footer{
+ }
+ .size-control {
display: flex;
align-items: center;
justify-content: center;
- margin-top: 20px;
- }
- .sub-right-footer{
- display: flex;
- align-items: center;
- justify-content: flex-end;
- margin-top: 20px;
- }
-}
-.pagination-wrap{
- margin-top: 24px;
-}
-
-.infomation-wrap{
- margin-bottom: 30px;
-}
-
-.infomation-box-wrap{
- display: flex;
- gap: 10px;
- .sub-table-box{
- flex: 1 ;
- }
- .info-title{
- font-size: 14px;
- font-weight: 500;
- color: #344356;
- margin-bottom: 10px;
- }
- .info-inner{
- position: relative;
- font-size: 13px;
- color: #344356;
- .copy-ico{
- position: absolute;
- bottom: 0;
- right: 0;
- width: 16px;
- height: 16px;
- background: url(../../public/static/images/sub/copy_ico.svg)no-repeat center;
- background-size: cover;
- }
- }
-}
-
-// 견적서
-.estimate-list-wrap{
- display: flex;
- align-items: center;
- margin-bottom: 10px;
- &.one{
- .estimate-box{
- &:last-child{
- flex: 1;
- min-width: unset;
- }
- }
- }
- .estimate-box{
- flex: 1 ;
- display: flex;
- align-items: center;
- &:last-child{
- flex: none;
- min-width: 220px;
- }
- .estimate-tit{
- width: 105px;
- height: 30px;
- line-height: 30px;
- background-color: #F4F4F7;
- border-radius: 100px;
- text-align: center;
- font-size: 13px;
- font-weight: 500;
- color: #344356;
- }
- .estimate-name{
- font-size: 13px;
- color: #344356;
- margin-left: 14px;
- font-weight: 400;
- &.blue{
- font-size: 16px;
- font-weight: 700;
- color: #1083E3;
- }
- &.red{
- font-size: 16px;
- font-weight: 700;
- color: #D72A2A;
- }
- }
- }
- &:last-child{
- margin-bottom: 0;
- }
-}
-
-// file drag box
-.drag-file-guide{
- font-size: 13px;
- font-weight: 400;
- color: #45576F;
- margin-left: 5px;
-}
-.btn-area{
- padding-bottom: 15px;
- border-bottom: 1px solid #ECF0F4;
- .file-upload{
- display: inline-block;
- height: 30px;
- background-color: #94A0AD;
- padding: 0 10px;
+ gap: 10px;
+ background-color: #3d3d3d;
border-radius: 2px;
- font-size: 13px;
- line-height: 30px;
- color: #fff;
- font-weight: 500;
- cursor: pointer;
- transition: background .15s ease-in-out;
- &:hover{
- background-color: #607F9A;
+ width: 100px;
+ height: 30px;
+ margin: 0 5px;
+ span {
+ font-size: 13px;
+ color: #fff;
+ cursor: pointer;
}
- }
-}
-.drag-file-box{
- padding: 10px;
- .drag-file-area{
- position: relative;
- margin-top: 15px;
- p{
- position: absolute;
- top: 50%;
- left: 50%;
- transform: translate(-50%, -50%);
- font-size: 13px;
- color: #ccc;
- font-weight: 400;
- cursor: default;
+ .control-btn {
+ display: block;
+ width: 12px;
+ height: 12px;
+ background-repeat: no-repeat;
+ background-size: cover;
+ background-position: center;
+ &.minus {
+ background-image: url(../../public/static/images/canvas/minus.svg);
+ }
+ &.plus {
+ background-image: url(../../public/static/images/canvas/plus.svg);
+ }
}
+ }
}
- .file-list{
- min-height: 52px;
- .file-item{
- margin-bottom: 15px;
- span{
- position: relative;
- font-size: 13px;
- color: #45576F;
- font-weight: 400;
- white-space: nowrap;
- padding-right: 55px;
- cursor: pointer;
- button{
- position: absolute;
- top: 50%;
- right: 0;
- transform: translateY(-50%);
- width: 15px;
- height: 15px;
- background: url(../../public/static/images/sub/file_delete.svg)no-repeat center;
- background-size: cover;
- }
- }
- &:last-child{
- margin-bottom: 0;
- }
- .file-item-wrap{
- display: flex;
- align-items: center;
- gap: 30px;
- .return-wrap{
- display: flex;
- align-items: center;
- }
- .return{
- padding: 0;
- font-size: 13px;
- color: #B0BCCD;
- text-decoration: line-through;
- }
- .return-btn{
- flex: none;
- position: relative;
- top: 0;
- left: 0;
- transform: none;
- display: flex;
- align-items: center;
- height: 24px;
- padding: 0 9px;
- margin-left: 10px;
- background: none;
- border: 1px solid #B0BCCD;
- border-radius: 2px;
- font-size: 12px;
- color: #B0BCCD;
- font-weight: 500;
- .return-ico{
- display: block;
- width: 14px;
- height: 14px;
- background: url(../../public/static/images/canvas/return-btn.svg)no-repeat center;
- background-size: contain;
- margin-right: 5px;
- }
- }
- }
- }
- }
-}
-
-.estimate-arr-btn{
- display: block;
- width: 20px;
- height: 20px;
- background-color: #94A0AD;
- border: 1px solid #94A0AD;
- background-position: center;
- background-repeat: no-repeat;
- background-image: url(../../public/static/images/canvas/estiment_arr.svg);
- background-size: 11px 7px;
- border-radius: 2px;
- &.up{
- rotate: 180deg;
- }
- &.on{
- background-color: #fff;
- border-color: #C2D0DD;
- background-image: url(../../public/static/images/canvas/estiment_arr_color.svg)
- }
-}
-.estimate-check-wrap{
- .estimate-check-inner{
- display: block;
- }
- &.hide{
- border-bottom: 1px solid #ECF0F4;
- margin-bottom: 15px;
- .estimate-check-inner{
- display: none;
- }
- }
-}
-
-.special-note-check-wrap{
- display: grid;
- grid-template-columns: repeat(5, 1fr);
- border-radius: 3px;
- margin-bottom: 30px;
- .special-note-check-item{
- padding: 14px 10px;
- border: 1px solid #ECF0F4;
- margin-top: -1px;
- margin-right: -1px;
- &.act{
- background-color: #F7F9FA;
- }
- .special-note-check-box{
- display: flex;
- align-items: center;
- .check-name{
- font-size: 13px;
- color: #45576F;
- cursor: pointer;
- line-height: 1.3;
- }
- }
- }
-}
-
-.calculation-estimate{
- border: 1px solid #ECF0F4;
- border-radius: 3px;
- padding: 24px;
- height: 170px;
- overflow-y: auto;
- margin-bottom: 30px;
- dl{
- margin-bottom: 35px;
- &:last-child{
- margin-bottom: 0;
- }
- dt{
- font-size: 13px;
- font-weight: 600;
- color: #1083E3;
- margin-bottom: 15px;
- }
- dd{
- font-size: 12px;
- font-weight: 400;
- color: #45576F;
- margin-bottom: 8px;
- &:last-child{
- margin-bottom: 0;
- }
- }
- }
- &::-webkit-scrollbar {
- width: 4px;
- background-color: transparent;
- }
- &::-webkit-scrollbar-thumb {
- background-color: #d9dee2;
- }
- &::-webkit-scrollbar-track {
- background-color: transparent;
- }
-}
-.esimate-wrap{
- margin-bottom: 20px;
-}
-
-.estimate-product-option{
- display: flex;
- align-items: center;
- margin-bottom: 15px;
- .product-price-wrap{
- display: flex;
- align-items: center;
- .product-price-tit{
- font-size: 13px;
- font-weight: 400;
- color: #45576F;
- margin-right: 10px;
- }
- .select-wrap{
- width: 150px;
- }
- }
- .product-edit-wrap{
- display: flex;
- align-items: center;
- margin-left: auto;
- .product-edit-explane{
- display: flex;
- align-items: center;
- margin-right: 15px;
- .explane-item{
- position: relative;
- display: flex;
- align-items: center;
- padding: 0 10px;
- font-size: 12px;
- font-weight: 400;
- span{
- width: 20px;
- height: 20px;
- margin-right: 5px;
- background-size: cover;
- background-repeat: no-repeat;
- background-position: center;
- }
- &:before{
- content: '';
- position: absolute;
- top: 50%;
- left: 0;
- transform: translateY(-50%);
- width: 1px;
- height: 12px;
- background-color: #D9D9D9;
- }
- &:first-child{
- padding-left: 0;
- &::before{
- display: none;
- }
- }
- &:last-child{
- padding-right: 0;
- }
- &.item01{
- color: #3BBB48;
- span{
- background-image: url(../../public/static/images/sub/open_ico.svg);
- }
- }
- &.item02{
- color: #909000;
- span{
- background-image: url(../../public/static/images/sub/change_ico.svg);
- }
- }
- &.item03{
- color: #0191C9;
- span{
- background-image: url(../../public/static/images/sub/attachment_ico.svg);
- }
- }
- &.item04{
- color: #F16A6A;
- span{
- background-image: url(../../public/static/images/sub/click_check_ico.svg);
- }
- }
- }
- }
- .product-edit-btn{
- display: flex;
- align-items: center;
- button{
- display: flex;
- align-items: center;
- span{
- width: 13px;
- height: 13px;
- margin-right: 5px;
- background-size: cover;
- &.plus{
- background: url(../../public/static/images/sub/plus_btn.svg)no-repeat center;
- }
- &.minus{
- background: url(../../public/static/images/sub/minus_btn.svg)no-repeat center;
- }
- }
- }
- }
- }
-}
-
-// 발전시물레이션
-.chart-wrap{
- display: flex;
- gap: 20px;
+ }
+ .canvas-depth2-wrap {
+ position: absolute;
+ top: -100%;
+ left: 0;
+ background-color: #383838;
width: 100%;
- .sub-table-box{
- height: 100%;
- }
- .chart-inner{
- flex: 1;
- .chart-box{
- margin-bottom: 30px;
- }
- }
- .chart-table-wrap{
+ height: 50px;
+ transition: all 0.17s ease-in-out;
+ .canvas-depth2-inner {
+ display: flex;
+ align-items: center;
+ padding: 0 40px;
+ height: 100%;
+ .canvas-depth2-list {
display: flex;
- flex-direction: column;
- flex: none;
- width: 650px;
- .sub-table-box{
- flex: 1;
- &:first-child{
- margin-bottom: 20px;
- }
- }
- }
-}
-
-.chart-month-table{
- table{
- table-layout: fixed;
- border-collapse:collapse;
- border: 1px solid #ECF0F4;
- border-radius: 4px;
- thead{
- th{
- padding: 4.5px 0;
- border-bottom: 1px solid #ECF0F4;
- text-align: center;
- font-size: 13px;
- color: #45576F;
- font-weight: 500;
- background-color: #F8F9FA;
- }
- }
- tbody{
- td{
- font-size: 13px;
- color: #45576F;
- text-align: center;
- padding: 4.5px 0;
- }
- }
- }
-}
-
-.simulation-guide-wrap{
- display: flex;
- padding: 20px;
- .simulation-tit-wrap{
- flex: none;
- padding-right: 40px;
- border-right: 1px solid #EEEEEE;
- span{
- display: block;
+ align-items: center;
+ height: 100%;
+ .canvas-depth2-item {
+ display: flex;
+ align-items: center;
+ margin-right: 26px;
+ height: 100%;
+ button {
position: relative;
- padding-left: 60px;
- font-size: 15px;
- color: #14324F;
- font-weight: 600;
- &::before{
+ opacity: 0.55;
+ color: #fff;
+ font-size: 12px;
+ font-weight: normal;
+ height: 100%;
+ padding-right: 12px;
+ }
+ &.active {
+ button {
+ opacity: 1;
+ font-weight: 600;
+ &:after {
content: '';
position: absolute;
top: 50%;
- left: 0;
+ right: 0;
transform: translateY(-50%);
- width: 40px;
- height: 40px;
- background: url(../../public/static/images/sub/simulation_guide.svg)no-repeat center;
- background-size: cover;
+ width: 5px;
+ height: 8px;
+ background: url(../../public/static/images/canvas/depth2-arr.svg) no-repeat center;
+ }
}
+ }
}
- }
- .simulation-guide-box{
- flex: 1;
- padding-left: 40px;
- dl{
- margin-bottom: 25px;
- dt{
- font-size: 13px;
- color: #101010;
- font-weight: 600;
- margin-bottom: 5px;
- }
- dd{
- font-size: 12px;
- color: #45576F;
- font-weight: 400;
- line-height: 24px;
- }
- &:last-child{
- margin-bottom: 0;
- }
- }
- ul, ol{
- list-style: unset;
- }
- }
-}
-
-.module-total{
- display: flex;
- align-items: center;
- background-color: #F8F9FA;
- padding: 9px 0;
- margin-right: 4px;
- border: 1px solid #ECF0F4;
- border-top: none;
- .total-title{
- flex: 1;
- text-align: center;
- font-size: 13px;
- color: #344356;
- font-weight: 500;
- }
- .total-num{
- flex: none;
- width: 121px;
- text-align: center;
- font-size: 15px;
- color: #344356;
- font-weight: 500;
- }
-}
-
-// 물건상세
-.information-help-wrap{
- display: flex;
- padding: 24px;
- background-color: #F4F4F4;
- border-radius: 4px;
- margin-bottom: 15px;
- .information-help-tit-wrap{
- position: relative;
+ }
+ .canvas-depth2-btn-list {
display: flex;
align-items: center;
- padding-right: 40px;
- border-right: 1px solid #E0E0E3;
- .help-tit-icon{
- width: 40px;
- height: 40px;
- border-radius: 50%;
- margin-right: 10px;
- background: #fff url(../../public/static/images/sub/information_help.svg)no-repeat center;
- background-size: 20px 20px;
- }
- .help-tit{
- font-size: 13px;
- font-weight: 600;
- color: #45576F;
- }
- }
- .information-help-guide{
- padding-left: 40px;
- span{
- display: block;
+ margin-left: auto;
+ height: 100%;
+ .depth2-btn-box {
+ display: flex;
+ align-items: center;
+ margin-right: 34px;
+ height: 100%;
+ transition: all 0.17s ease-in-out;
+ button {
+ position: relative;
font-size: 12px;
font-weight: 400;
- color: #45576F;
- margin-bottom: 7px;
- &:last-child{
- margin-bottom: 0;
+ height: 100%;
+ color: #fff;
+ padding-right: 12px;
+ &:after {
+ content: '';
+ position: absolute;
+ top: 50%;
+ right: 0;
+ transform: translateY(-50%);
+ width: 5px;
+ height: 8px;
+ background: url(../../public/static/images/canvas/depth2-arr.svg) no-repeat center;
}
+ }
+ &:last-child {
+ margin-right: 0;
+ }
+ &.mouse {
+ opacity: 0.55;
+ }
}
+ }
}
+ &.active {
+ top: 47px;
+ }
+ }
+ &.active {
+ padding-bottom: 50px;
+ }
}
-.community-search-warp{
+// canvas-layout
+.canvas-content {
+ padding-top: 46.8px;
+ transition: all 0.17s ease-in-out;
+ .canvas-frame {
+ height: calc(100vh - 129.3px);
+ }
+ &.active {
+ padding-top: calc(46.8px + 50px);
+ .canvas-frame {
+ height: calc(100vh - 179.4px);
+ }
+ }
+}
+.canvas-layout {
+ padding-top: 37px;
+ .canvas-page-list {
+ position: fixed;
+ top: 92.8px;
+ left: 0;
+ display: flex;
+ background-color: #1c1c1c;
+ border-top: 1px solid #000;
+ width: 100%;
+ min-width: 1280px;
+ transition: all 0.17s ease-in-out;
+ z-index: 99;
+ &.active {
+ top: calc(92.8px + 50px);
+ }
+ .canvas-id {
+ display: flex;
+ align-items: center;
+ padding: 9.6px 20px;
+ font-size: 12px;
+ color: #fff;
+ background-color: #1083e3;
+ }
+ .canvas-plane-wrap {
+ display: flex;
+ align-items: center;
+ max-width: calc(100% - 45px);
+ .canvas-page-box {
+ display: flex;
+ align-items: center;
+ background-color: #1c1c1c;
+ padding: 9.6px 20px;
+ border-right: 1px solid #000;
+ min-width: 0;
+ transition: all 0.17s ease-in-out;
+ span {
+ display: flex;
+ align-items: center;
+ width: 100%;
+ font-size: 12px;
+ font-family: 'Pretendard', sans-serif;
+ color: #aaa;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ }
+ .close {
+ flex: none;
+ display: block;
+ width: 7px;
+ height: 8px;
+ margin-left: 15px;
+ background: url(../../public/static/images/canvas/plan_close_gray.svg) no-repeat center;
+ background-size: cover;
+ }
+ &.on {
+ background-color: #fff;
+ span {
+ font-weight: 600;
+ color: #101010;
+ }
+ .close {
+ background: url(../../public/static/images/canvas/plan_close_black.svg) no-repeat center;
+ }
+ &:hover {
+ background-color: #fff;
+ }
+ }
+ &:hover {
+ background-color: #000;
+ }
+ }
+ }
+ .plane-add {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 45px;
+ padding: 13.5px 0;
+ background-color: #1c1c1c;
+ border-right: 1px solid #000;
+ transition: all 0.17s ease-in-out;
+ span {
+ display: block;
+ width: 9px;
+ height: 9px;
+ background: url(../../public/static/images/canvas/plane_add.svg) no-repeat center;
+ background-size: cover;
+ }
+ &:hover {
+ background-color: #000;
+ }
+ }
+ }
+}
+
+.canvas-frame {
+ position: relative;
+ // height: calc(100% - 36.5px);
+ background-color: #f4f4f7;
+ overflow: auto;
+ transition: all 0.17s ease-in-out;
+ // &::-webkit-scrollbar {
+ // width: 10px;
+ // height: 10px;
+ // background-color: #fff;
+ // }
+ // &::-webkit-scrollbar-thumb {
+ // background-color: #C1CCD7;
+ // border-radius: 30px;
+ // }
+ // &::-webkit-scrollbar-track {
+ // background-color: #fff;
+ // }
+ .canvas-container {
+ margin: 0 auto;
+ background-color: #fff;
+ }
+ canvas {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ }
+}
+
+// sub-page
+.sub-header {
+ position: fixed;
+ top: 46px;
+ left: 0;
+ width: 100%;
+ min-width: 1280px;
+ height: 46px;
+ border-bottom: 1px solid #000;
+ background: #2c2c2c;
+ z-index: 999;
+ .sub-header-inner {
display: flex;
- flex-direction: column;
align-items: center;
- padding: 10px 0 30px 0;
- border-bottom: 1px solid #E5E5E5;
- margin-bottom: 24px;
- .community-search-box{
+ height: 100%;
+ padding: 0 100px;
+ .sub-header-title-wrap {
+ display: flex;
+ align-items: center;
+ .title-item {
+ position: relative;
+ padding: 0 24px;
+ a {
+ display: flex;
+ align-items: center;
+ .icon {
+ width: 22px;
+ height: 22px;
+ margin-right: 8px;
+ background-repeat: no-repeat;
+ background-position: center;
+ background-size: cover;
+ &.drawing {
+ background-image: url(../../public/static/images/main/drawing_icon.svg);
+ }
+ }
+ }
+ &:after {
+ content: '';
+ position: absolute;
+ top: 50%;
+ right: 0;
+ transform: translateY(-50%);
+ width: 1px;
+ height: 16px;
+ background-color: #d9d9d9;
+ }
+ &:first-child {
+ padding-left: 0;
+ }
+ &:last-child {
+ padding-right: 0;
+ &:after {
+ display: none;
+ }
+ }
+ }
+ }
+ .sub-header-title {
+ font-size: 16px;
+ color: #fff;
+ font-weight: 600;
+ }
+ .sub-header-location {
+ margin-left: auto;
+ display: flex;
+ align-items: center;
+ .location-item {
position: relative;
display: flex;
align-items: center;
- width: 580px;
- height: 45px;
- padding: 0 45px 0 20px;
- margin-bottom: 20px;
- border-radius: 2px;
- border: 1px solid #101010;
- .community-input{
- width: 100%;
- height: 100%;
- font-size: 13px;
- font-weight: 400;
- color: #101010;
- &::placeholder{
- color: #C8C8C8;
- }
+ padding: 0 10px;
+ span {
+ display: flex;
+ font-size: 12px;
+ color: #aaa;
+ font-weight: normal;
+ cursor: default;
}
- .community-search-ico{
- position: absolute;
- top: 50%;
- right: 20px;
- transform: translateY(-50%);
- flex: none;
- width: 21px;
- height: 100%;
- background: url(../../public/static/images/sub/community_search.svg)no-repeat center;
- background-size: 21px 21px;
- z-index: 3;
- }
- }
- .community-search-keyword{
- font-size: 13px;
- font-weight: 400;
- color: #45576F;
- span{
- font-weight: 600;
- color: #F16A6A;
+ &:after {
+ content: '';
+ position: absolute;
+ top: 50%;
+ right: 0;
+ transform: translateY(-50%);
+ width: 4px;
+ height: 6px;
+ background: url(../../public/static/images/main/loaction_arr.svg) no-repeat center;
}
+ &:first-child {
+ padding-left: 0;
+ }
+ &:last-child {
+ padding-right: 0;
+ span {
+ color: #fff;
+ }
+ &:after {
+ display: none;
+ }
+ }
+ }
}
+ }
}
-// 자료 다운로드
-.file-down-list{
- display: grid;
- grid-template-columns: repeat(3, 1fr);
- gap: 14px;
- .file-down-item{
+// sub content
+.sub-content {
+ padding-top: 46px;
+ .sub-content-inner {
+ max-width: 1760px;
+ margin: 0 auto;
+ padding: 20px 20px 0;
+ .sub-content-box {
+ margin-bottom: 20px;
+ &:last-child {
+ margin-bottom: 0;
+ }
+ }
+ }
+ &.estimate {
+ display: flex;
+ flex-direction: column;
+ padding-top: 0;
+ .sub-content-inner {
+ flex: 1;
+ width: 100%;
+ }
+ }
+}
+.sub-table-box {
+ padding: 20px;
+ border-radius: 6px;
+ border: 1px solid #e9eaed;
+ background: #fff;
+ box-shadow: 0px 3px 30px 0px rgba(0, 0, 0, 0.02);
+ .table-box-title-wrap {
+ display: flex;
+ align-items: center;
+ margin-bottom: 15px;
+ .title-wrap {
+ display: flex;
+ align-items: center;
+ h3 {
+ display: block;
+ font-size: 15px;
+ color: #101010;
+ font-weight: 600;
+ margin-right: 14px;
+ &.product {
+ margin-right: 10px;
+ }
+ }
+ .estimate-check-btn {
+ position: relative;
display: flex;
align-items: center;
- padding: 24px;
- border-radius: 4px;
- border: 1px solid #E5E5E5;
- background: #FFF;
- transition: all .15s ease-in-out;
- .file-item-info{
- .item-num{
- display: inline-block;
- padding: 6px 17.5px;
- border-radius: 60px;
- background-color: #F4F4F7;
- font-size: 13px;
- font-weight: 600;
- color: #101010;
- margin-bottom: 15px;
- }
- .item-name{
- font-size: 16px;
- color: #101010;
- font-weight: 500;
- margin-bottom: 13px;
- }
- .item-date{
- font-size: 13px;
- font-weight: 400;
- color: #344356;
- }
+ padding-left: 10px;
+ &::before {
+ content: '';
+ position: absolute;
+ top: 50%;
+ left: 0;
+ transform: translateY(-50%);
+ width: 1px;
+ height: 11px;
+ background-color: #d9d9d9;
}
- .file-down-box{
- display: flex;
- align-items: center;
- flex: none;
- margin-left: auto;
- height: 100%;
- .file-down-btn{
- width: 36px;
- height: 36px;
- background: url(../../public/static/images/sub/file_down_btn.svg)no-repeat center;
- background-size: cover;
+ }
+ .product_tit {
+ position: relative;
+ font-size: 15px;
+ font-weight: 600;
+ color: #1083e3;
+ padding-left: 10px;
+ &::before {
+ content: '';
+ position: absolute;
+ top: 50%;
+ left: 0;
+ transform: translateY(-50%);
+ width: 1px;
+ height: 11px;
+ background-color: #d9d9d9;
+ }
+ }
+ .option {
+ padding-left: 5px;
+ font-size: 13px;
+ color: #101010;
+ font-weight: 400;
+ }
+ .info-wrap {
+ display: flex;
+ align-items: center;
+ li {
+ position: relative;
+ padding: 0 6px;
+ font-size: 12px;
+ color: #101010;
+ font-weight: normal;
+ span {
+ font-weight: 600;
+ &.red {
+ color: #e23d70;
}
+ }
+ &:after {
+ content: '';
+ position: absolute;
+ top: 50%;
+ right: 0;
+ transform: translateY(-50%);
+ width: 1px;
+ height: 11px;
+ background-color: #d9d9d9;
+ }
+ &:first-child {
+ padding-left: 0;
+ }
+ &:last-child {
+ padding-right: 0;
+ &::after {
+ display: none;
+ }
+ }
}
- &:hover{
- background-color: #F4F4F7;
- }
+ }
}
-}
-
-.file-down-nodata{
+ }
+ .left-unit-box {
+ margin-left: auto;
+ display: flex;
+ align-items: center;
+ }
+ .promise-title-wrap {
+ display: flex;
+ align-items: center;
+ margin-bottom: 15px;
+ .promise-gudie {
+ margin-bottom: 0;
+ }
+ }
+ .promise-gudie {
+ display: block;
+ font-size: 13px;
+ font-weight: 700;
+ color: #101010;
+ margin-bottom: 20px;
+ }
+ .important {
+ color: #f00;
+ }
+ .sub-center-footer {
display: flex;
align-items: center;
justify-content: center;
- width: 100%;
- height: 148px;
- padding: 24px;
- border-radius: 4px;
- border: 1px solid #E5E5E5;
- font-size: 16px;
+ margin-top: 20px;
+ }
+ .sub-right-footer {
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+ margin-top: 20px;
+ }
+}
+.pagination-wrap {
+ margin-top: 24px;
+}
+
+.infomation-wrap {
+ margin-bottom: 30px;
+}
+
+.infomation-box-wrap {
+ display: flex;
+ gap: 10px;
+ .sub-table-box {
+ flex: 1;
+ }
+ .info-title {
+ font-size: 14px;
font-weight: 500;
color: #344356;
+ margin-bottom: 10px;
+ }
+ .info-inner {
+ position: relative;
+ font-size: 13px;
+ color: #344356;
+ .copy-ico {
+ position: absolute;
+ bottom: 0;
+ right: 0;
+ width: 16px;
+ height: 16px;
+ background: url(../../public/static/images/sub/copy_ico.svg) no-repeat center;
+ background-size: cover;
+ }
+ }
+}
+
+// 견적서
+.estimate-list-wrap {
+ display: flex;
+ align-items: center;
+ margin-bottom: 10px;
+ &.one {
+ .estimate-box {
+ &:last-child {
+ flex: 1;
+ min-width: unset;
+ }
+ }
+ }
+ .estimate-box {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ &:last-child {
+ flex: none;
+ min-width: 220px;
+ }
+ .estimate-tit {
+ width: 105px;
+ height: 30px;
+ line-height: 30px;
+ background-color: #f4f4f7;
+ border-radius: 100px;
+ text-align: center;
+ font-size: 13px;
+ font-weight: 500;
+ color: #344356;
+ }
+ .estimate-name {
+ font-size: 13px;
+ color: #344356;
+ margin-left: 14px;
+ font-weight: 400;
+ &.blue {
+ font-size: 16px;
+ font-weight: 700;
+ color: #1083e3;
+ }
+ &.red {
+ font-size: 16px;
+ font-weight: 700;
+ color: #d72a2a;
+ }
+ }
+ }
+ &:last-child {
+ margin-bottom: 0;
+ }
+}
+
+// file drag box
+.drag-file-guide {
+ font-size: 13px;
+ font-weight: 400;
+ color: #45576f;
+ margin-left: 5px;
+}
+.btn-area {
+ padding-bottom: 15px;
+ border-bottom: 1px solid #ecf0f4;
+ .file-upload {
+ display: inline-block;
+ height: 30px;
+ background-color: #94a0ad;
+ padding: 0 10px;
+ border-radius: 2px;
+ font-size: 13px;
+ line-height: 30px;
+ color: #fff;
+ font-weight: 500;
+ cursor: pointer;
+ transition: background 0.15s ease-in-out;
+ &:hover {
+ background-color: #607f9a;
+ }
+ }
+}
+.drag-file-box {
+ padding: 10px;
+ .drag-file-area {
+ position: relative;
+ margin-top: 15px;
+ p {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ font-size: 13px;
+ color: #ccc;
+ font-weight: 400;
+ cursor: default;
+ }
+ }
+ .file-list {
+ min-height: 52px;
+ .file-item {
+ margin-bottom: 15px;
+ span {
+ position: relative;
+ font-size: 13px;
+ color: #45576f;
+ font-weight: 400;
+ white-space: nowrap;
+ padding-right: 55px;
+ cursor: pointer;
+ button {
+ position: absolute;
+ top: 50%;
+ right: 0;
+ transform: translateY(-50%);
+ width: 15px;
+ height: 15px;
+ background: url(../../public/static/images/sub/file_delete.svg) no-repeat center;
+ background-size: cover;
+ }
+ }
+ &:last-child {
+ margin-bottom: 0;
+ }
+ .file-item-wrap {
+ display: flex;
+ align-items: center;
+ gap: 30px;
+ .return-wrap {
+ display: flex;
+ align-items: center;
+ }
+ .return {
+ padding: 0;
+ font-size: 13px;
+ color: #b0bccd;
+ text-decoration: line-through;
+ }
+ .return-btn {
+ flex: none;
+ position: relative;
+ top: 0;
+ left: 0;
+ transform: none;
+ display: flex;
+ align-items: center;
+ height: 24px;
+ padding: 0 9px;
+ margin-left: 10px;
+ background: none;
+ border: 1px solid #b0bccd;
+ border-radius: 2px;
+ font-size: 12px;
+ color: #b0bccd;
+ font-weight: 500;
+ .return-ico {
+ display: block;
+ width: 14px;
+ height: 14px;
+ background: url(../../public/static/images/canvas/return-btn.svg) no-repeat center;
+ background-size: contain;
+ margin-right: 5px;
+ }
+ }
+ }
+ }
+ }
+}
+
+.estimate-arr-btn {
+ display: block;
+ width: 20px;
+ height: 20px;
+ background-color: #94a0ad;
+ border: 1px solid #94a0ad;
+ background-position: center;
+ background-repeat: no-repeat;
+ background-image: url(../../public/static/images/canvas/estiment_arr.svg);
+ background-size: 11px 7px;
+ border-radius: 2px;
+ &.up {
+ rotate: 180deg;
+ }
+ &.on {
+ background-color: #fff;
+ border-color: #c2d0dd;
+ background-image: url(../../public/static/images/canvas/estiment_arr_color.svg);
+ }
+}
+.estimate-check-wrap {
+ .estimate-check-inner {
+ display: block;
+ }
+ &.hide {
+ border-bottom: 1px solid #ecf0f4;
+ margin-bottom: 15px;
+ .estimate-check-inner {
+ display: none;
+ }
+ }
+}
+
+.special-note-check-wrap {
+ display: grid;
+ grid-template-columns: repeat(5, 1fr);
+ border-radius: 3px;
+ margin-bottom: 30px;
+ .special-note-check-item {
+ padding: 14px 10px;
+ border: 1px solid #ecf0f4;
+ margin-top: -1px;
+ margin-right: -1px;
+ &.act {
+ background-color: #f7f9fa;
+ }
+ .special-note-check-box {
+ display: flex;
+ align-items: center;
+ .check-name {
+ font-size: 13px;
+ color: #45576f;
+ cursor: pointer;
+ line-height: 1.3;
+ }
+ }
+ }
+}
+
+.calculation-estimate {
+ border: 1px solid #ecf0f4;
+ border-radius: 3px;
+ padding: 24px;
+ height: 170px;
+ overflow-y: auto;
+ margin-bottom: 30px;
+ dl {
+ margin-bottom: 35px;
+ &:last-child {
+ margin-bottom: 0;
+ }
+ dt {
+ font-size: 13px;
+ font-weight: 600;
+ color: #1083e3;
+ margin-bottom: 15px;
+ }
+ dd {
+ font-size: 12px;
+ font-weight: 400;
+ color: #45576f;
+ margin-bottom: 8px;
+ &:last-child {
+ margin-bottom: 0;
+ }
+ }
+ }
+ &::-webkit-scrollbar {
+ width: 4px;
+ background-color: transparent;
+ }
+ &::-webkit-scrollbar-thumb {
+ background-color: #d9dee2;
+ }
+ &::-webkit-scrollbar-track {
+ background-color: transparent;
+ }
+}
+.esimate-wrap {
+ margin-bottom: 20px;
+}
+
+.estimate-product-option {
+ display: flex;
+ align-items: center;
+ margin-bottom: 15px;
+ .product-price-wrap {
+ display: flex;
+ align-items: center;
+ .product-price-tit {
+ font-size: 13px;
+ font-weight: 400;
+ color: #45576f;
+ margin-right: 10px;
+ }
+ .select-wrap {
+ width: 150px;
+ }
+ }
+ .product-edit-wrap {
+ display: flex;
+ align-items: center;
+ margin-left: auto;
+ .product-edit-explane {
+ display: flex;
+ align-items: center;
+ margin-right: 15px;
+ .explane-item {
+ position: relative;
+ display: flex;
+ align-items: center;
+ padding: 0 10px;
+ font-size: 12px;
+ font-weight: 400;
+ span {
+ width: 20px;
+ height: 20px;
+ margin-right: 5px;
+ background-size: cover;
+ background-repeat: no-repeat;
+ background-position: center;
+ }
+ &:before {
+ content: '';
+ position: absolute;
+ top: 50%;
+ left: 0;
+ transform: translateY(-50%);
+ width: 1px;
+ height: 12px;
+ background-color: #d9d9d9;
+ }
+ &:first-child {
+ padding-left: 0;
+ &::before {
+ display: none;
+ }
+ }
+ &:last-child {
+ padding-right: 0;
+ }
+ &.item01 {
+ color: #3bbb48;
+ span {
+ background-image: url(../../public/static/images/sub/open_ico.svg);
+ }
+ }
+ &.item02 {
+ color: #909000;
+ span {
+ background-image: url(../../public/static/images/sub/change_ico.svg);
+ }
+ }
+ &.item03 {
+ color: #0191c9;
+ span {
+ background-image: url(../../public/static/images/sub/attachment_ico.svg);
+ }
+ }
+ &.item04 {
+ color: #f16a6a;
+ span {
+ background-image: url(../../public/static/images/sub/click_check_ico.svg);
+ }
+ }
+ }
+ }
+ .product-edit-btn {
+ display: flex;
+ align-items: center;
+ button {
+ display: flex;
+ align-items: center;
+ span {
+ width: 13px;
+ height: 13px;
+ margin-right: 5px;
+ background-size: cover;
+ &.plus {
+ background: url(../../public/static/images/sub/plus_btn.svg) no-repeat center;
+ }
+ &.minus {
+ background: url(../../public/static/images/sub/minus_btn.svg) no-repeat center;
+ }
+ }
+ }
+ }
+ }
+}
+
+// 발전시물레이션
+.chart-wrap {
+ display: flex;
+ gap: 20px;
+ width: 100%;
+ .sub-table-box {
+ height: 100%;
+ }
+ .chart-inner {
+ flex: 1;
+ .chart-box {
+ margin-bottom: 30px;
+ }
+ }
+ .chart-table-wrap {
+ display: flex;
+ flex-direction: column;
+ flex: none;
+ width: 650px;
+ .sub-table-box {
+ flex: 1;
+ &:first-child {
+ margin-bottom: 20px;
+ }
+ }
+ }
+}
+
+.chart-month-table {
+ table {
+ table-layout: fixed;
+ border-collapse: collapse;
+ border: 1px solid #ecf0f4;
+ border-radius: 4px;
+ thead {
+ th {
+ padding: 4.5px 0;
+ border-bottom: 1px solid #ecf0f4;
+ text-align: center;
+ font-size: 13px;
+ color: #45576f;
+ font-weight: 500;
+ background-color: #f8f9fa;
+ }
+ }
+ tbody {
+ td {
+ font-size: 13px;
+ color: #45576f;
+ text-align: center;
+ padding: 4.5px 0;
+ }
+ }
+ }
+}
+
+.simulation-guide-wrap {
+ display: flex;
+ padding: 20px;
+ .simulation-tit-wrap {
+ flex: none;
+ padding-right: 40px;
+ border-right: 1px solid #eeeeee;
+ span {
+ display: block;
+ position: relative;
+ padding-left: 60px;
+ font-size: 15px;
+ color: #14324f;
+ font-weight: 600;
+ &::before {
+ content: '';
+ position: absolute;
+ top: 50%;
+ left: 0;
+ transform: translateY(-50%);
+ width: 40px;
+ height: 40px;
+ background: url(../../public/static/images/sub/simulation_guide.svg) no-repeat center;
+ background-size: cover;
+ }
+ }
+ }
+ .simulation-guide-box {
+ flex: 1;
+ padding-left: 40px;
+ dl {
+ margin-bottom: 25px;
+ dt {
+ font-size: 13px;
+ color: #101010;
+ font-weight: 600;
+ margin-bottom: 5px;
+ }
+ dd {
+ font-size: 12px;
+ color: #45576f;
+ font-weight: 400;
+ line-height: 24px;
+ }
+ &:last-child {
+ margin-bottom: 0;
+ }
+ }
+ ul,
+ ol {
+ list-style: unset;
+ }
+ }
+}
+
+.module-total {
+ display: flex;
+ align-items: center;
+ background-color: #f8f9fa;
+ padding: 9px 0;
+ margin-right: 4px;
+ border: 1px solid #ecf0f4;
+ border-top: none;
+ .total-title {
+ flex: 1;
+ text-align: center;
+ font-size: 13px;
+ color: #344356;
+ font-weight: 500;
+ }
+ .total-num {
+ flex: none;
+ width: 121px;
+ text-align: center;
+ font-size: 15px;
+ color: #344356;
+ font-weight: 500;
+ }
+}
+
+// 물건상세
+.information-help-wrap {
+ display: flex;
+ padding: 24px;
+ background-color: #f4f4f4;
+ border-radius: 4px;
+ margin-bottom: 15px;
+ .information-help-tit-wrap {
+ position: relative;
+ display: flex;
+ align-items: center;
+ padding-right: 40px;
+ border-right: 1px solid #e0e0e3;
+ .help-tit-icon {
+ width: 40px;
+ height: 40px;
+ border-radius: 50%;
+ margin-right: 10px;
+ background: #fff url(../../public/static/images/sub/information_help.svg) no-repeat center;
+ background-size: 20px 20px;
+ }
+ .help-tit {
+ font-size: 13px;
+ font-weight: 600;
+ color: #45576f;
+ }
+ }
+ .information-help-guide {
+ padding-left: 40px;
+ span {
+ display: block;
+ font-size: 12px;
+ font-weight: 400;
+ color: #45576f;
+ margin-bottom: 7px;
+ &:last-child {
+ margin-bottom: 0;
+ }
+ }
+ }
+}
+
+.community-search-warp {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ padding: 10px 0 30px 0;
+ border-bottom: 1px solid #e5e5e5;
+ margin-bottom: 24px;
+ .community-search-box {
+ position: relative;
+ display: flex;
+ align-items: center;
+ width: 580px;
+ height: 45px;
+ padding: 0 45px 0 20px;
+ margin-bottom: 20px;
+ border-radius: 2px;
+ border: 1px solid #101010;
+ .community-input {
+ width: 100%;
+ height: 100%;
+ font-size: 13px;
+ font-weight: 400;
+ color: #101010;
+ &::placeholder {
+ color: #c8c8c8;
+ }
+ }
+ .community-search-ico {
+ position: absolute;
+ top: 50%;
+ right: 20px;
+ transform: translateY(-50%);
+ flex: none;
+ width: 21px;
+ height: 100%;
+ background: url(../../public/static/images/sub/community_search.svg) no-repeat center;
+ background-size: 21px 21px;
+ z-index: 3;
+ }
+ }
+ .community-search-keyword {
+ font-size: 13px;
+ font-weight: 400;
+ color: #45576f;
+ span {
+ font-weight: 600;
+ color: #f16a6a;
+ }
+ }
+}
+
+// 자료 다운로드
+.file-down-list {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: 14px;
+ .file-down-item {
+ display: flex;
+ align-items: center;
+ padding: 24px;
+ border-radius: 4px;
+ border: 1px solid #e5e5e5;
+ background: #fff;
+ transition: all 0.15s ease-in-out;
+ .file-item-info {
+ .item-num {
+ display: inline-block;
+ padding: 6px 17.5px;
+ border-radius: 60px;
+ background-color: #f4f4f7;
+ font-size: 13px;
+ font-weight: 600;
+ color: #101010;
+ margin-bottom: 15px;
+ }
+ .item-name {
+ font-size: 16px;
+ color: #101010;
+ font-weight: 500;
+ margin-bottom: 13px;
+ }
+ .item-date {
+ font-size: 13px;
+ font-weight: 400;
+ color: #344356;
+ }
+ }
+ .file-down-box {
+ display: flex;
+ align-items: center;
+ flex: none;
+ margin-left: auto;
+ height: 100%;
+ .file-down-btn {
+ width: 36px;
+ height: 36px;
+ background: url(../../public/static/images/sub/file_down_btn.svg) no-repeat center;
+ background-size: cover;
+ }
+ }
+ &:hover {
+ background-color: #f4f4f7;
+ }
+ }
+}
+
+.file-down-nodata {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 100%;
+ height: 148px;
+ padding: 24px;
+ border-radius: 4px;
+ border: 1px solid #e5e5e5;
+ font-size: 16px;
+ font-weight: 500;
+ color: #344356;
}
//신규물건 등록
-.product-input-wrap{
- display: flex;
- align-items: center;
- width: 200px;
- height: 30px;
- background-color: #FAFAFA;
- border: 1px solid #EEE;
- padding: 0 10px;
- input{
- font-size: 13px;
- font-weight: 400;
- color: #999999;
- padding: 0;
- width: 100%;
- height: 100%;
- flex: 1 ;
- background-color: inherit;
- }
- .product-delete{
- flex: none;
- display: block;
- width: 15px;
- height: 100%;
- background: url(../../public/static/images/sub/product-del.svg)no-repeat center;
- background-size: 15px 15px;
- }
+.product-input-wrap {
+ display: flex;
+ align-items: center;
+ width: 200px;
+ height: 30px;
+ background-color: #fafafa;
+ border: 1px solid #eee;
+ padding: 0 10px;
+ input {
+ font-size: 13px;
+ font-weight: 400;
+ color: #999999;
+ padding: 0;
+ width: 100%;
+ height: 100%;
+ flex: 1;
+ background-color: inherit;
+ }
+ .product-delete {
+ flex: none;
+ display: block;
+ width: 15px;
+ height: 100%;
+ background: url(../../public/static/images/sub/product-del.svg) no-repeat center;
+ background-size: 15px 15px;
+ }
}
@media screen and (max-width: 1800px) {
- .canvas-menu-wrap{
- .canvas-menu-inner{
- .canvas-menu-list{
- .canvas-menu-item button{
- .menu-icon{
- margin-right: 5px;
- }
- }
- .canvas-menu-item{
- button{
- padding: 15px 15px;
- font-size: 11px;
- }
- }
- }
+ .canvas-menu-wrap {
+ .canvas-menu-inner {
+ .canvas-menu-list {
+ .canvas-menu-item button {
+ .menu-icon {
+ margin-right: 5px;
+ }
}
- .canvas-depth2-wrap{
- .canvas-depth2-inner{
- .canvas-depth2-list{
- .canvas-depth2-item{
- button{
- font-size: 11px;
- }
- }
- }
- }
+ .canvas-menu-item {
+ button {
+ padding: 15px 15px;
+ font-size: 11px;
+ }
}
- }
+ }
+ }
+ .canvas-depth2-wrap {
+ .canvas-depth2-inner {
+ .canvas-depth2-list {
+ .canvas-depth2-item {
+ button {
+ font-size: 11px;
+ }
+ }
+ }
+ }
+ }
+ }
}
@media screen and (max-width: 1600px) {
- .canvas-menu-wrap{
- .canvas-menu-inner{
- .canvas-menu-list{
- .canvas-menu-item button{
- .menu-icon{
- display: none;
- }
- }
- }
+ .canvas-menu-wrap {
+ .canvas-menu-inner {
+ .canvas-menu-list {
+ .canvas-menu-item button {
+ .menu-icon {
+ display: none;
+ }
}
- }
- .canvas-content{
- .canvas-frame{
- height: calc(100vh - 129.5px);
- }
- &.active{
- .canvas-frame{
- height: calc(100vh - 179.5px);
- }
- }
- }
+ }
+ }
+ }
+ .canvas-content {
+ .canvas-frame {
+ height: calc(100vh - 129.5px);
+ }
+ &.active {
+ .canvas-frame {
+ height: calc(100vh - 179.5px);
+ }
+ }
+ }
}
@media screen and (max-width: 1500px) {
- .canvas-menu-wrap{
- .canvas-menu-inner{
- .canvas-menu-list{
- .canvas-menu-item{
- button{
- padding: 15px 10px;
- font-size: 10px;
- }
- }
- }
- .canvas-side-btn-wrap{
- .btn-from{
- gap: 3px;
- }
- .vertical-horizontal{
- margin-right: 3px;
- min-width: 150px;
- }
- .select-box{
- width: 100px;
- margin: 0 3px;
- }
- .size-control{
- width: 90px;
- margin: 0 3px;
- }
- }
+ .canvas-menu-wrap {
+ .canvas-menu-inner {
+ .canvas-menu-list {
+ .canvas-menu-item {
+ button {
+ padding: 15px 10px;
+ font-size: 10px;
+ }
}
+ }
+ .canvas-side-btn-wrap {
+ .btn-from {
+ gap: 3px;
+ }
+ .vertical-horizontal {
+ margin-right: 3px;
+ min-width: 150px;
+ }
+ .select-box {
+ width: 100px;
+ margin: 0 3px;
+ }
+ .size-control {
+ width: 90px;
+ margin: 0 3px;
+ }
+ }
}
- .sub-header{
- .sub-header-inner{
- .sub-header-title{
- font-size: 15px;
+ }
+ .sub-header {
+ .sub-header-inner {
+ .sub-header-title {
+ font-size: 15px;
+ }
+ .sub-header-title-wrap {
+ .title-item {
+ a {
+ .icon {
+ width: 20px;
+ height: 20px;
}
- .sub-header-title-wrap{
- .title-item{
- a{
- .icon{
- width: 20px;
- height: 20px;
- }
- }
- }
- }
- }
- }
-
+ }
+ }
+ }
+ }
+ }
}
diff --git a/startscript-btob.js b/startscript-btob.js
new file mode 100644
index 00000000..61ff89c1
--- /dev/null
+++ b/startscript-btob.js
@@ -0,0 +1,2 @@
+var exec = require('child_process').exec
+exec('yarn start:btob', { windowsHide: true })