diff --git a/.env.development b/.env.development index 8f653e38..6b3844f8 100644 --- a/.env.development +++ b/.env.development @@ -36,4 +36,5 @@ S3_PROFILE="dev" NEXT_PUBLIC_ENABLE_LOGGING=true #Gemini API Key -GEMINI_API_KEY="AIzaSyDDmMkTkNt12QTj7I3SUygYkOoeG_Sc28Q" \ No newline at end of file +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 a436dafe..50904369 100644 --- a/.env.production +++ b/.env.production @@ -35,4 +35,5 @@ S3_PROFILE="prd" NEXT_PUBLIC_ENABLE_LOGGING=false #Gemini API Key -GEMINI_API_KEY="AIzaSyDDmMkTkNt12QTj7I3SUygYkOoeG_Sc28Q" \ No newline at end of file +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 faa32996..7a1234d4 100644 --- a/.gitignore +++ b/.gitignore @@ -56,6 +56,9 @@ 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, docs, .agents 및 .claude 하위의 md 는 추적 유지 *.md diff --git a/package.json b/package.json index 1f96d08c..e21464e5 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ }, "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)} /> ))} + {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')} + +
+
+ + +
+
+ + {pdfFile && !pdfAnalyzing && } +
+
+
+ {getMessage('modal.placement.initial.setting.size.pdf.file.info')} +
+ + + + {getMessage('modal.placement.initial.setting.size.pdf.page.label')} + +
+
+ setPdfPageMode(e.target.value)} + /> + +
+
+ setPdfPageMode(e.target.value)} + /> + +
+ {pdfPageMode === PDF_PAGE_MODE.SPECIFY && ( + <> + {getMessage('modal.placement.initial.setting.size.pdf.page.facade.label')} + setPdfFacadePages(e.target.value)} + /> + {getMessage('modal.placement.initial.setting.size.pdf.page.floor.label')} + setPdfFloorPages(e.target.value)} + /> + + )} +
+
+ {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
-
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; - } - } - } - } - } - } - + } + } + } + } + } }