feat(pdf-gemini): 배치면 초기설정에 PDF 도면 업로드 옵션 추가 — Gemini 외곽선 분석
- 「배치면 초기설정」 모달 寸法入力方法 라디오에 '도면 파일 업로드' 옵션 추가 - 선택 시 PDF 파일 업로드 + 읽기 페이지 설정(전체/입면도·평면도 지정) UI 노출 - 保存 클릭 시 /api/gemini/floor-plan 호출 → 외곽선을 캔버스에 반영한 후 기존 저장 흐름 진행 - Gemini File API 업로드 + finally 파일 폐기, 정점/면적 검증, 페이지 힌트 프롬프트 주입 - usePdfImport 훅: mm→canvas 좌표 변환, CCW 정규화, 기존 외곽선 덮어쓰기 confirm 후 QLine/QPolygon 생성 - @google/generative-ai 의존성 추가, GEMINI_MODEL env 4개 환경에 동기화 - 다국어(ja/ko) 메시지 추가, .playwright-mcp/ gitignore Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
c1247a95db
commit
849f7a1dc9
@ -37,3 +37,4 @@ NEXT_PUBLIC_ENABLE_LOGGING=true
|
|||||||
|
|
||||||
#Gemini API Key
|
#Gemini API Key
|
||||||
GEMINI_API_KEY="AIzaSyDDmMkTkNt12QTj7I3SUygYkOoeG_Sc28Q"
|
GEMINI_API_KEY="AIzaSyDDmMkTkNt12QTj7I3SUygYkOoeG_Sc28Q"
|
||||||
|
GEMINI_MODEL="gemini-3.1-pro-preview"
|
||||||
@ -30,3 +30,7 @@ AWS_SECRET_ACCESS_KEY="Cw87TjKwnTWRKgORGxYiFU6GUTgu25eUw4eLBNcA"
|
|||||||
NEXT_PUBLIC_AWS_S3_BASE_URL="//files.hanasys.jp"
|
NEXT_PUBLIC_AWS_S3_BASE_URL="//files.hanasys.jp"
|
||||||
|
|
||||||
S3_PROFILE="dev"
|
S3_PROFILE="dev"
|
||||||
|
|
||||||
|
#Gemini API Key
|
||||||
|
GEMINI_API_KEY="AIzaSyDDmMkTkNt12QTj7I3SUygYkOoeG_Sc28Q"
|
||||||
|
GEMINI_MODEL="gemini-3.1-pro-preview"
|
||||||
@ -35,3 +35,7 @@ S3_PROFILE="dev"
|
|||||||
#logging
|
#logging
|
||||||
# [debug auto-capture 2026-05-06] true 로 켜면 debugCapture 가 debug/*.json/*.log 로 자동 저장.
|
# [debug auto-capture 2026-05-06] true 로 켜면 debugCapture 가 debug/*.json/*.log 로 자동 저장.
|
||||||
NEXT_PUBLIC_ENABLE_LOGGING=true
|
NEXT_PUBLIC_ENABLE_LOGGING=true
|
||||||
|
|
||||||
|
#Gemini API Key
|
||||||
|
GEMINI_API_KEY="AIzaSyDDmMkTkNt12QTj7I3SUygYkOoeG_Sc28Q"
|
||||||
|
GEMINI_MODEL="gemini-3.1-pro-preview"
|
||||||
|
|||||||
@ -36,3 +36,4 @@ NEXT_PUBLIC_ENABLE_LOGGING=false
|
|||||||
|
|
||||||
#Gemini API Key
|
#Gemini API Key
|
||||||
GEMINI_API_KEY="AIzaSyDDmMkTkNt12QTj7I3SUygYkOoeG_Sc28Q"
|
GEMINI_API_KEY="AIzaSyDDmMkTkNt12QTj7I3SUygYkOoeG_Sc28Q"
|
||||||
|
GEMINI_MODEL="gemini-3.1-pro-preview"
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
@ -56,6 +56,9 @@ sl
|
|||||||
# [graphify 2026-05-12] graphify 지식 그래프 산출물 (로컬 캐시, post-commit hook 으로 자동 갱신)
|
# [graphify 2026-05-12] graphify 지식 그래프 산출물 (로컬 캐시, post-commit hook 으로 자동 갱신)
|
||||||
graphify-out/
|
graphify-out/
|
||||||
|
|
||||||
|
# [playwright-mcp] Playwright MCP 임시 산출물 (스크린샷, 콘솔 로그)
|
||||||
|
.playwright-mcp/
|
||||||
|
|
||||||
# [local-only 2026-05-08] 모든 markdown 문서는 git 추적 제외 (로컬 전용)
|
# [local-only 2026-05-08] 모든 markdown 문서는 git 추적 제외 (로컬 전용)
|
||||||
# 단, 루트 README.md / CLAUDE.md / AGENTS.md, docs, .agents 및 .claude 하위의 md 는 추적 유지
|
# 단, 루트 README.md / CLAUDE.md / AGENTS.md, docs, .agents 및 .claude 하위의 md 는 추적 유지
|
||||||
*.md
|
*.md
|
||||||
|
|||||||
@ -19,6 +19,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-s3": "^3.772.0",
|
"@aws-sdk/client-s3": "^3.772.0",
|
||||||
|
"@google/generative-ai": "^0.24.1",
|
||||||
"ag-grid-react": "^32.0.2",
|
"ag-grid-react": "^32.0.2",
|
||||||
"axios": "^1.7.8",
|
"axios": "^1.7.8",
|
||||||
"big.js": "^6.2.2",
|
"big.js": "^6.2.2",
|
||||||
|
|||||||
210
src/app/api/gemini/floor-plan/route.js
Normal file
210
src/app/api/gemini/floor-plan/route.js
Normal file
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -23,12 +23,27 @@ import { useRoofFn } from '@/hooks/common/useRoofFn'
|
|||||||
import { usePlan } from '@/hooks/usePlan'
|
import { usePlan } from '@/hooks/usePlan'
|
||||||
import { normalizeDecimal } from '@/util/input-utils'
|
import { normalizeDecimal } from '@/util/input-utils'
|
||||||
import { CalculatorInput } from '@/components/common/input/CalcInput'
|
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
|
// [LOW-PITCH-WARN 2026-05-06] 저구배 + 특정 기와 사용 시 시공 매뉴얼 안내 alert
|
||||||
import { useSwal } from '@/hooks/useSwal'
|
import { useSwal } from '@/hooks/useSwal'
|
||||||
import { notifyLowPitchRestrictionForRoofs, isLowPitchRestricted, LOW_PITCH_RESTRICTED_ROOF_IDS } from '@/util/roof-pitch-warning'
|
import { notifyLowPitchRestrictionForRoofs, isLowPitchRestricted, LOW_PITCH_RESTRICTED_ROOF_IDS } from '@/util/roof-pitch-warning'
|
||||||
// [LOW-PITCH-DIAG 2026-05-06 TEMP] 진단용 — 검증 후 import + 호출 함께 제거
|
// [LOW-PITCH-DIAG 2026-05-06 TEMP] 진단용 — 검증 후 import + 호출 함께 제거
|
||||||
import { debugCapture } from '@/util/debugCapture'
|
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()
|
const { saveCanvas } = usePlan()
|
||||||
// [LOW-PITCH-WARN 2026-05-06]
|
// [LOW-PITCH-WARN 2026-05-06]
|
||||||
const { swalFire } = useSwal()
|
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 = [
|
const roofSizeSetArray = [
|
||||||
{ id: 'ra01', name: 'roofSizeSet', value: '1', message: 'modal.placement.initial.setting.size.roof' },
|
{ id: 'ra01', name: 'roofSizeSet', value: INPUT_MODE.SIZE_ROOF, message: 'modal.placement.initial.setting.size.roof' },
|
||||||
{ id: 'ra02', name: 'roofSizeSet', value: '2', message: 'modal.placement.initial.setting.size.actual' },
|
{ id: 'ra02', name: 'roofSizeSet', value: INPUT_MODE.SIZE_ACTUAL, message: 'modal.placement.initial.setting.size.actual' },
|
||||||
// { id: 'ra03', name: 'roofSizeSet', value: '3', message: 'modal.placement.initial.setting.size.none.pitch' },
|
{ 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(() => {
|
useEffect(() => {
|
||||||
if (currentCanvasPlan?.planNo && currentRoof) {
|
if (currentCanvasPlan?.planNo && currentRoof) {
|
||||||
setCurrentRoof(prev => ({ ...prev, planNo: currentCanvasPlan.planNo }))
|
setCurrentRoof((prev) => ({ ...prev, planNo: currentCanvasPlan.planNo }))
|
||||||
}
|
}
|
||||||
}, [currentCanvasPlan?.planNo])
|
}, [currentCanvasPlan?.planNo])
|
||||||
|
|
||||||
@ -238,6 +350,13 @@ export default function PlacementShapeSetting({ id, pos = { x: 50, y: 180 }, pla
|
|||||||
* 배치면초기설정 저장 버튼 클릭
|
* 배치면초기설정 저장 버튼 클릭
|
||||||
*/
|
*/
|
||||||
const handleSaveBtn = async () => {
|
const handleSaveBtn = async () => {
|
||||||
|
if (pdfAnalyzing) return
|
||||||
|
|
||||||
|
if (inputMode === INPUT_MODE.PDF) {
|
||||||
|
const ok = await analyzePdfAndApply()
|
||||||
|
if (!ok) return
|
||||||
|
}
|
||||||
|
|
||||||
const roofInfo = {
|
const roofInfo = {
|
||||||
...currentRoof,
|
...currentRoof,
|
||||||
planNo: currentCanvasPlan?.planNo || basicSetting.planNo,
|
planNo: currentCanvasPlan?.planNo || basicSetting.planNo,
|
||||||
@ -273,7 +392,8 @@ export default function PlacementShapeSetting({ id, pos = { x: 50, y: 180 }, pla
|
|||||||
/**
|
/**
|
||||||
* 배치면초기설정 저장 (메뉴 변경/useEffect 트리거 없이)
|
* 배치면초기설정 저장 (메뉴 변경/useEffect 트리거 없이)
|
||||||
*/
|
*/
|
||||||
basicSettingSave({
|
basicSettingSave(
|
||||||
|
{
|
||||||
...basicSetting,
|
...basicSetting,
|
||||||
planNo: currentCanvasPlan?.planNo || basicSetting.planNo,
|
planNo: currentCanvasPlan?.planNo || basicSetting.planNo,
|
||||||
/**
|
/**
|
||||||
@ -282,7 +402,9 @@ export default function PlacementShapeSetting({ id, pos = { x: 50, y: 180 }, pla
|
|||||||
selectedRoofMaterial: {
|
selectedRoofMaterial: {
|
||||||
...newAddedRoofs[0],
|
...newAddedRoofs[0],
|
||||||
},
|
},
|
||||||
}, { skipSideEffects: true })
|
},
|
||||||
|
{ skipSideEffects: true },
|
||||||
|
)
|
||||||
|
|
||||||
const roofs = canvas.getObjects().filter((obj) => obj.roofMaterial?.index === 0)
|
const roofs = canvas.getObjects().filter((obj) => obj.roofMaterial?.index === 0)
|
||||||
|
|
||||||
@ -354,15 +476,119 @@ export default function PlacementShapeSetting({ id, pos = { x: 50, y: 180 }, pla
|
|||||||
id={item.id}
|
id={item.id}
|
||||||
name={item.name}
|
name={item.name}
|
||||||
value={item.value}
|
value={item.value}
|
||||||
checked={String(currentRoof?.roofSizeSet) === item.value}
|
checked={inputMode === item.value}
|
||||||
onChange={(e) => setCurrentRoof({ ...currentRoof, roofSizeSet: e.target.value })}
|
onChange={(e) => handleInputModeChange(e.target.value)}
|
||||||
/>
|
/>
|
||||||
<label htmlFor={item.id}>{getMessage(item.message)}</label>
|
<label htmlFor={item.id}>{getMessage(item.message)}</label>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
{inputMode === INPUT_MODE.PDF && (
|
||||||
|
<div
|
||||||
|
className="pdf-info-text"
|
||||||
|
style={{ fontSize: '12px', lineHeight: 1.5, color: '#bbb', whiteSpace: 'pre-line', marginTop: '8px' }}
|
||||||
|
>
|
||||||
|
{getMessage('modal.placement.initial.setting.size.pdf.info')}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
{inputMode === INPUT_MODE.PDF && (
|
||||||
|
<>
|
||||||
|
<tr>
|
||||||
|
<th>{getMessage('modal.placement.initial.setting.size.pdf.file.label')}</th>
|
||||||
|
<td>
|
||||||
|
<div className="img-flex-box" style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
|
||||||
|
<div className="img-edit-wrap">
|
||||||
|
<label className={`img-edit-btn ${pdfAnalyzing ? 'no-click' : ''}`} htmlFor="pdf_import_file">
|
||||||
|
<span className="img-edit"></span>
|
||||||
|
{getMessage('modal.placement.initial.setting.size.pdf.file.upload')}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
ref={pdfInputRef}
|
||||||
|
type="file"
|
||||||
|
id="pdf_import_file"
|
||||||
|
accept="application/pdf"
|
||||||
|
style={{ display: 'none' }}
|
||||||
|
disabled={pdfAnalyzing}
|
||||||
|
onChange={handlePdfFileChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="img-name-wrap" style={{ flex: 1, display: 'flex', alignItems: 'center' }}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="input-origin al-l"
|
||||||
|
value={pdfFile?.name ?? ''}
|
||||||
|
readOnly
|
||||||
|
placeholder={getMessage('modal.placement.initial.setting.size.pdf.file.placeholder')}
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
/>
|
||||||
|
{pdfFile && !pdfAnalyzing && <button className="img-check" onClick={resetPdfFile}></button>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: '12px', color: '#bbb', marginTop: '4px' }}>
|
||||||
|
{getMessage('modal.placement.initial.setting.size.pdf.file.info')}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>{getMessage('modal.placement.initial.setting.size.pdf.page.label')}</th>
|
||||||
|
<td>
|
||||||
|
<div className="pop-form-radio" style={{ alignItems: 'center', gap: '12px' }}>
|
||||||
|
<div className="d-check-radio pop">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
id="pdf-page-all"
|
||||||
|
name="pdfPageMode"
|
||||||
|
value={PDF_PAGE_MODE.ALL}
|
||||||
|
checked={pdfPageMode === PDF_PAGE_MODE.ALL}
|
||||||
|
onChange={(e) => setPdfPageMode(e.target.value)}
|
||||||
|
/>
|
||||||
|
<label htmlFor="pdf-page-all">{getMessage('modal.placement.initial.setting.size.pdf.page.all')}</label>
|
||||||
|
</div>
|
||||||
|
<div className="d-check-radio pop">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
id="pdf-page-specify"
|
||||||
|
name="pdfPageMode"
|
||||||
|
value={PDF_PAGE_MODE.SPECIFY}
|
||||||
|
checked={pdfPageMode === PDF_PAGE_MODE.SPECIFY}
|
||||||
|
onChange={(e) => setPdfPageMode(e.target.value)}
|
||||||
|
/>
|
||||||
|
<label htmlFor="pdf-page-specify">{getMessage('modal.placement.initial.setting.size.pdf.page.specify')}</label>
|
||||||
|
</div>
|
||||||
|
{pdfPageMode === PDF_PAGE_MODE.SPECIFY && (
|
||||||
|
<>
|
||||||
|
<span style={{ fontSize: '13px' }}>{getMessage('modal.placement.initial.setting.size.pdf.page.facade.label')}</span>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="input-origin"
|
||||||
|
style={{ width: '90px' }}
|
||||||
|
placeholder={getMessage('modal.placement.initial.setting.size.pdf.page.example.facade')}
|
||||||
|
value={pdfFacadePages}
|
||||||
|
onChange={(e) => setPdfFacadePages(e.target.value)}
|
||||||
|
/>
|
||||||
|
<span style={{ fontSize: '13px' }}>{getMessage('modal.placement.initial.setting.size.pdf.page.floor.label')}</span>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="input-origin"
|
||||||
|
style={{ width: '90px' }}
|
||||||
|
placeholder={getMessage('modal.placement.initial.setting.size.pdf.page.example.floor')}
|
||||||
|
value={pdfFloorPages}
|
||||||
|
onChange={(e) => setPdfFloorPages(e.target.value)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: '12px', color: '#bbb', marginTop: '4px' }}>
|
||||||
|
{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')}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<tr>
|
<tr>
|
||||||
<th>
|
<th>
|
||||||
<div className="tip-wrap">
|
<div className="tip-wrap">
|
||||||
@ -630,10 +856,19 @@ export default function PlacementShapeSetting({ id, pos = { x: 50, y: 180 }, pla
|
|||||||
</div>
|
</div>
|
||||||
<div className="grid-btn-wrap" style={{ display: 'flex', alignItems: 'center', justifyContent: 'flex-end', gap: '5px' }}>
|
<div className="grid-btn-wrap" style={{ display: 'flex', alignItems: 'center', justifyContent: 'flex-end', gap: '5px' }}>
|
||||||
<button style={{ width: 'auto' }} onClick={() => setUseCalcPad((prev) => !prev)}>
|
<button style={{ width: 'auto' }} onClick={() => setUseCalcPad((prev) => !prev)}>
|
||||||
<Image src={useCalcPad ? '/static/images/common/Icon_ON_Black.png' : '/static/images/common/Icon_OFF_Black.png'} alt="toggle" width={34} height={34} />
|
<Image
|
||||||
|
src={useCalcPad ? '/static/images/common/Icon_ON_Black.png' : '/static/images/common/Icon_OFF_Black.png'}
|
||||||
|
alt="toggle"
|
||||||
|
width={34}
|
||||||
|
height={34}
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
<button className="btn-frame modal act" onClick={handleSaveBtn}>
|
<button
|
||||||
{getMessage('modal.common.save')}
|
className={`btn-frame modal act ${pdfAnalyzing || (inputMode === INPUT_MODE.PDF && !pdfFile) ? 'no-click' : ''}`}
|
||||||
|
onClick={handleSaveBtn}
|
||||||
|
disabled={pdfAnalyzing || (inputMode === INPUT_MODE.PDF && !pdfFile)}
|
||||||
|
>
|
||||||
|
{pdfAnalyzing ? getMessage('pdf.import.button.analyzing') : getMessage('modal.common.save')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</WithDraggable.Body>
|
</WithDraggable.Body>
|
||||||
|
|||||||
176
src/hooks/pdf-import/usePdfImport.js
Normal file
176
src/hooks/pdf-import/usePdfImport.js
Normal file
@ -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 }
|
||||||
|
}
|
||||||
@ -16,6 +16,33 @@
|
|||||||
"header.stem": "Stem",
|
"header.stem": "Stem",
|
||||||
"plan.menu.plan.drawing": "物件情報",
|
"plan.menu.plan.drawing": "物件情報",
|
||||||
"plan.menu.placement.surface.initial.setting": "配置面初期設定",
|
"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": "図面の作成方法",
|
||||||
"modal.placement.initial.setting.plan.drawing.size.stuff": "寸法入力による物件作成",
|
"modal.placement.initial.setting.plan.drawing.size.stuff": "寸法入力による物件作成",
|
||||||
"modal.placement.initial.setting.plan.drawing.size.info": "※数字は[半角]入力のみ可能です。",
|
"modal.placement.initial.setting.plan.drawing.size.info": "※数字は[半角]入力のみ可能です。",
|
||||||
|
|||||||
@ -16,6 +16,33 @@
|
|||||||
"header.stem": "Stem",
|
"header.stem": "Stem",
|
||||||
"plan.menu.plan.drawing": "물건정보",
|
"plan.menu.plan.drawing": "물건정보",
|
||||||
"plan.menu.placement.surface.initial.setting": "배치면 초기설정",
|
"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": "도면 작성방법",
|
||||||
"modal.placement.initial.setting.plan.drawing.size.stuff": "치수 입력에 의한 물건 작성",
|
"modal.placement.initial.setting.plan.drawing.size.stuff": "치수 입력에 의한 물건 작성",
|
||||||
"modal.placement.initial.setting.plan.drawing.size.info": "※숫자는 [반각] 입력만 가능합니다.",
|
"modal.placement.initial.setting.plan.drawing.size.info": "※숫자는 [반각] 입력만 가능합니다.",
|
||||||
|
|||||||
@ -26,14 +26,14 @@
|
|||||||
min-width: 1280px;
|
min-width: 1280px;
|
||||||
padding-bottom: 0;
|
padding-bottom: 0;
|
||||||
background-color: #383838;
|
background-color: #383838;
|
||||||
transition: padding .17s ease-in-out;
|
transition: padding 0.17s ease-in-out;
|
||||||
z-index: 999;
|
z-index: 999;
|
||||||
.canvas-menu-inner {
|
.canvas-menu-inner {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 0 40px 0 20px;
|
padding: 0 40px 0 20px;
|
||||||
background-color: #2C2C2C;
|
background-color: #2c2c2c;
|
||||||
height: 46.8px;
|
height: 46.8px;
|
||||||
z-index: 999;
|
z-index: 999;
|
||||||
.canvas-menu-list {
|
.canvas-menu-list {
|
||||||
@ -53,7 +53,7 @@
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
padding: 15px 20px;
|
padding: 15px 20px;
|
||||||
opacity: 0.55;
|
opacity: 0.55;
|
||||||
transition: all .17s ease-in-out;
|
transition: all 0.17s ease-in-out;
|
||||||
.menu-icon {
|
.menu-icon {
|
||||||
display: block;
|
display: block;
|
||||||
width: 14px;
|
width: 14px;
|
||||||
@ -62,13 +62,27 @@
|
|||||||
background-position: center;
|
background-position: center;
|
||||||
background-size: contain;
|
background-size: contain;
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
&.con00{background-image: url(/static/images/canvas/menu_icon00.svg);}
|
&.con00 {
|
||||||
&.con01{background-image: url(/static/images/canvas/menu_icon01.svg);}
|
background-image: url(/static/images/canvas/menu_icon00.svg);
|
||||||
&.con02{background-image: url(/static/images/canvas/menu_icon02.svg);}
|
}
|
||||||
&.con03{background-image: url(/static/images/canvas/menu_icon03.svg);}
|
&.con01 {
|
||||||
&.con04{background-image: url(/static/images/canvas/menu_icon04.svg);}
|
background-image: url(/static/images/canvas/menu_icon01.svg);
|
||||||
&.con05{background-image: url(/static/images/canvas/menu_icon05.svg);}
|
}
|
||||||
&.con06{background-image: url(/static/images/canvas/menu_icon06.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 {
|
&.active {
|
||||||
@ -100,26 +114,47 @@
|
|||||||
width: 30px;
|
width: 30px;
|
||||||
height: 30px;
|
height: 30px;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
background-color: #3D3D3D;
|
background-color: #3d3d3d;
|
||||||
background-position: center;
|
background-position: center;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
background-size: 15px 15px;
|
background-size: 15px 15px;
|
||||||
transition: all .17s ease-in-out;
|
transition: all 0.17s ease-in-out;
|
||||||
&.btn01{background-image: url(../../public/static/images/canvas/side_icon03.svg);}
|
&.btn01 {
|
||||||
&.btn02{background-image: url(../../public/static/images/canvas/side_icon02.svg);}
|
background-image: url(../../public/static/images/canvas/side_icon03.svg);
|
||||||
&.btn03{background-image: url(../../public/static/images/canvas/side_icon01.svg);}
|
}
|
||||||
&.btn04{background-image: url(../../public/static/images/canvas/side_icon04.svg);}
|
&.btn02 {
|
||||||
&.btn05{background-image: url(../../public/static/images/canvas/side_icon05.svg);}
|
background-image: url(../../public/static/images/canvas/side_icon02.svg);
|
||||||
&.btn06{background-image: url(../../public/static/images/canvas/side_icon06.svg);}
|
}
|
||||||
&.btn07{background-image: url(../../public/static/images/canvas/side_icon07.svg);}
|
&.btn03 {
|
||||||
&.btn08{background-image: url(../../public/static/images/canvas/side_icon08.svg);}
|
background-image: url(../../public/static/images/canvas/side_icon01.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;}
|
&.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 {
|
&:hover {
|
||||||
background-color: #1083E3;
|
background-color: #1083e3;
|
||||||
}
|
}
|
||||||
&.active {
|
&.active {
|
||||||
background-color: #1083E3;
|
background-color: #1083e3;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -135,11 +170,21 @@
|
|||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
background-position: center;
|
background-position: center;
|
||||||
background-size: contain;
|
background-size: contain;
|
||||||
&.ico01{background-image: url(../../public/static/images/canvas/ico-flx01.svg);}
|
&.ico01 {
|
||||||
&.ico02{background-image: url(../../public/static/images/canvas/ico-flx02.svg);}
|
background-image: url(../../public/static/images/canvas/ico-flx01.svg);
|
||||||
&.ico03{background-image: url(../../public/static/images/canvas/ico-flx03.svg);}
|
}
|
||||||
&.ico04{background-image: url(../../public/static/images/canvas/ico-flx04.svg);}
|
&.ico02 {
|
||||||
&.ico05{background-image: url(../../public/static/images/canvas/ico-flx05.svg);}
|
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 {
|
.name {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
@ -169,16 +214,16 @@
|
|||||||
button {
|
button {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background-color: #4B4B4B;
|
background-color: #4b4b4b;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
padding: 0 7.5px;
|
padding: 0 7.5px;
|
||||||
transition: all .17s ease-in-out;
|
transition: all 0.17s ease-in-out;
|
||||||
}
|
}
|
||||||
&.on {
|
&.on {
|
||||||
button {
|
button {
|
||||||
background-color: #1083E3;
|
background-color: #1083e3;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -187,7 +232,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
background-color: #3D3D3D;
|
background-color: #3d3d3d;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
width: 100px;
|
width: 100px;
|
||||||
height: 30px;
|
height: 30px;
|
||||||
@ -221,7 +266,7 @@
|
|||||||
background-color: #383838;
|
background-color: #383838;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 50px;
|
height: 50px;
|
||||||
transition: all .17s ease-in-out;
|
transition: all 0.17s ease-in-out;
|
||||||
.canvas-depth2-inner {
|
.canvas-depth2-inner {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -273,7 +318,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
margin-right: 34px;
|
margin-right: 34px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
transition: all .17s ease-in-out;
|
transition: all 0.17s ease-in-out;
|
||||||
button {
|
button {
|
||||||
position: relative;
|
position: relative;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
@ -313,7 +358,7 @@
|
|||||||
// canvas-layout
|
// canvas-layout
|
||||||
.canvas-content {
|
.canvas-content {
|
||||||
padding-top: 46.8px;
|
padding-top: 46.8px;
|
||||||
transition: all .17s ease-in-out;
|
transition: all 0.17s ease-in-out;
|
||||||
.canvas-frame {
|
.canvas-frame {
|
||||||
height: calc(100vh - 129.3px);
|
height: calc(100vh - 129.3px);
|
||||||
}
|
}
|
||||||
@ -331,11 +376,11 @@
|
|||||||
top: 92.8px;
|
top: 92.8px;
|
||||||
left: 0;
|
left: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
background-color: #1C1C1C;
|
background-color: #1c1c1c;
|
||||||
border-top: 1px solid #000;
|
border-top: 1px solid #000;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-width: 1280px;
|
min-width: 1280px;
|
||||||
transition: all .17s ease-in-out;
|
transition: all 0.17s ease-in-out;
|
||||||
z-index: 99;
|
z-index: 99;
|
||||||
&.active {
|
&.active {
|
||||||
top: calc(92.8px + 50px);
|
top: calc(92.8px + 50px);
|
||||||
@ -346,7 +391,7 @@
|
|||||||
padding: 9.6px 20px;
|
padding: 9.6px 20px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
background-color: #1083E3;
|
background-color: #1083e3;
|
||||||
}
|
}
|
||||||
.canvas-plane-wrap {
|
.canvas-plane-wrap {
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -359,14 +404,14 @@
|
|||||||
padding: 9.6px 20px;
|
padding: 9.6px 20px;
|
||||||
border-right: 1px solid #000;
|
border-right: 1px solid #000;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
transition: all .17s ease-in-out;
|
transition: all 0.17s ease-in-out;
|
||||||
span {
|
span {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-family: 'Pretendard', sans-serif;
|
font-family: 'Pretendard', sans-serif;
|
||||||
color: #AAA;
|
color: #aaa;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@ -404,9 +449,9 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 45px;
|
width: 45px;
|
||||||
padding: 13.5px 0;
|
padding: 13.5px 0;
|
||||||
background-color: #1C1C1C;
|
background-color: #1c1c1c;
|
||||||
border-right: 1px solid #000;
|
border-right: 1px solid #000;
|
||||||
transition: all .17s ease-in-out;
|
transition: all 0.17s ease-in-out;
|
||||||
span {
|
span {
|
||||||
display: block;
|
display: block;
|
||||||
width: 9px;
|
width: 9px;
|
||||||
@ -424,9 +469,9 @@
|
|||||||
.canvas-frame {
|
.canvas-frame {
|
||||||
position: relative;
|
position: relative;
|
||||||
// height: calc(100% - 36.5px);
|
// height: calc(100% - 36.5px);
|
||||||
background-color: #F4F4F7;
|
background-color: #f4f4f7;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
transition: all .17s ease-in-out;
|
transition: all 0.17s ease-in-out;
|
||||||
// &::-webkit-scrollbar {
|
// &::-webkit-scrollbar {
|
||||||
// width: 10px;
|
// width: 10px;
|
||||||
// height: 10px;
|
// height: 10px;
|
||||||
@ -461,7 +506,7 @@
|
|||||||
min-width: 1280px;
|
min-width: 1280px;
|
||||||
height: 46px;
|
height: 46px;
|
||||||
border-bottom: 1px solid #000;
|
border-bottom: 1px solid #000;
|
||||||
background: #2C2C2C;
|
background: #2c2c2c;
|
||||||
z-index: 999;
|
z-index: 999;
|
||||||
.sub-header-inner {
|
.sub-header-inner {
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -484,7 +529,9 @@
|
|||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
background-position: center;
|
background-position: center;
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
&.drawing{background-image: url(../../public/static/images/main/drawing_icon.svg);}
|
&.drawing {
|
||||||
|
background-image: url(../../public/static/images/main/drawing_icon.svg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
&:after {
|
&:after {
|
||||||
@ -495,7 +542,7 @@
|
|||||||
transform: translateY(-50%);
|
transform: translateY(-50%);
|
||||||
width: 1px;
|
width: 1px;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
background-color: #D9D9D9;
|
background-color: #d9d9d9;
|
||||||
}
|
}
|
||||||
&:first-child {
|
&:first-child {
|
||||||
padding-left: 0;
|
padding-left: 0;
|
||||||
@ -525,7 +572,7 @@
|
|||||||
span {
|
span {
|
||||||
display: flex;
|
display: flex;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #AAA;
|
color: #aaa;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
@ -583,8 +630,8 @@
|
|||||||
.sub-table-box {
|
.sub-table-box {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
border: 1px solid #E9EAED;
|
border: 1px solid #e9eaed;
|
||||||
background: #FFF;
|
background: #fff;
|
||||||
box-shadow: 0px 3px 30px 0px rgba(0, 0, 0, 0.02);
|
box-shadow: 0px 3px 30px 0px rgba(0, 0, 0, 0.02);
|
||||||
.table-box-title-wrap {
|
.table-box-title-wrap {
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -616,14 +663,14 @@
|
|||||||
transform: translateY(-50%);
|
transform: translateY(-50%);
|
||||||
width: 1px;
|
width: 1px;
|
||||||
height: 11px;
|
height: 11px;
|
||||||
background-color: #D9D9D9;
|
background-color: #d9d9d9;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.product_tit {
|
.product_tit {
|
||||||
position: relative;
|
position: relative;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #1083E3;
|
color: #1083e3;
|
||||||
padding-left: 10px;
|
padding-left: 10px;
|
||||||
&::before {
|
&::before {
|
||||||
content: '';
|
content: '';
|
||||||
@ -633,7 +680,7 @@
|
|||||||
transform: translateY(-50%);
|
transform: translateY(-50%);
|
||||||
width: 1px;
|
width: 1px;
|
||||||
height: 11px;
|
height: 11px;
|
||||||
background-color: #D9D9D9;
|
background-color: #d9d9d9;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.option {
|
.option {
|
||||||
@ -654,7 +701,7 @@
|
|||||||
span {
|
span {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
&.red {
|
&.red {
|
||||||
color: #E23D70;
|
color: #e23d70;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
&:after {
|
&:after {
|
||||||
@ -665,10 +712,17 @@
|
|||||||
transform: translateY(-50%);
|
transform: translateY(-50%);
|
||||||
width: 1px;
|
width: 1px;
|
||||||
height: 11px;
|
height: 11px;
|
||||||
background-color: #D9D9D9;
|
background-color: #d9d9d9;
|
||||||
|
}
|
||||||
|
&:first-child {
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
&:last-child {
|
||||||
|
padding-right: 0;
|
||||||
|
&::after {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
&:first-child{padding-left: 0;}
|
|
||||||
&:last-child{padding-right: 0;&::after{display: none;}}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -770,7 +824,7 @@
|
|||||||
width: 105px;
|
width: 105px;
|
||||||
height: 30px;
|
height: 30px;
|
||||||
line-height: 30px;
|
line-height: 30px;
|
||||||
background-color: #F4F4F7;
|
background-color: #f4f4f7;
|
||||||
border-radius: 100px;
|
border-radius: 100px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
@ -785,12 +839,12 @@
|
|||||||
&.blue {
|
&.blue {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: #1083E3;
|
color: #1083e3;
|
||||||
}
|
}
|
||||||
&.red {
|
&.red {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: #D72A2A;
|
color: #d72a2a;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -803,16 +857,16 @@
|
|||||||
.drag-file-guide {
|
.drag-file-guide {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
color: #45576F;
|
color: #45576f;
|
||||||
margin-left: 5px;
|
margin-left: 5px;
|
||||||
}
|
}
|
||||||
.btn-area {
|
.btn-area {
|
||||||
padding-bottom: 15px;
|
padding-bottom: 15px;
|
||||||
border-bottom: 1px solid #ECF0F4;
|
border-bottom: 1px solid #ecf0f4;
|
||||||
.file-upload {
|
.file-upload {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
height: 30px;
|
height: 30px;
|
||||||
background-color: #94A0AD;
|
background-color: #94a0ad;
|
||||||
padding: 0 10px;
|
padding: 0 10px;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
@ -820,9 +874,9 @@
|
|||||||
color: #fff;
|
color: #fff;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background .15s ease-in-out;
|
transition: background 0.15s ease-in-out;
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: #607F9A;
|
background-color: #607f9a;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -849,7 +903,7 @@
|
|||||||
span {
|
span {
|
||||||
position: relative;
|
position: relative;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: #45576F;
|
color: #45576f;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
padding-right: 55px;
|
padding-right: 55px;
|
||||||
@ -879,7 +933,7 @@
|
|||||||
.return {
|
.return {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: #B0BCCD;
|
color: #b0bccd;
|
||||||
text-decoration: line-through;
|
text-decoration: line-through;
|
||||||
}
|
}
|
||||||
.return-btn {
|
.return-btn {
|
||||||
@ -894,10 +948,10 @@
|
|||||||
padding: 0 9px;
|
padding: 0 9px;
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
background: none;
|
background: none;
|
||||||
border: 1px solid #B0BCCD;
|
border: 1px solid #b0bccd;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #B0BCCD;
|
color: #b0bccd;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
.return-ico {
|
.return-ico {
|
||||||
display: block;
|
display: block;
|
||||||
@ -917,8 +971,8 @@
|
|||||||
display: block;
|
display: block;
|
||||||
width: 20px;
|
width: 20px;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
background-color: #94A0AD;
|
background-color: #94a0ad;
|
||||||
border: 1px solid #94A0AD;
|
border: 1px solid #94a0ad;
|
||||||
background-position: center;
|
background-position: center;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
background-image: url(../../public/static/images/canvas/estiment_arr.svg);
|
background-image: url(../../public/static/images/canvas/estiment_arr.svg);
|
||||||
@ -929,8 +983,8 @@
|
|||||||
}
|
}
|
||||||
&.on {
|
&.on {
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
border-color: #C2D0DD;
|
border-color: #c2d0dd;
|
||||||
background-image: url(../../public/static/images/canvas/estiment_arr_color.svg)
|
background-image: url(../../public/static/images/canvas/estiment_arr_color.svg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.estimate-check-wrap {
|
.estimate-check-wrap {
|
||||||
@ -938,7 +992,7 @@
|
|||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
&.hide {
|
&.hide {
|
||||||
border-bottom: 1px solid #ECF0F4;
|
border-bottom: 1px solid #ecf0f4;
|
||||||
margin-bottom: 15px;
|
margin-bottom: 15px;
|
||||||
.estimate-check-inner {
|
.estimate-check-inner {
|
||||||
display: none;
|
display: none;
|
||||||
@ -953,18 +1007,18 @@
|
|||||||
margin-bottom: 30px;
|
margin-bottom: 30px;
|
||||||
.special-note-check-item {
|
.special-note-check-item {
|
||||||
padding: 14px 10px;
|
padding: 14px 10px;
|
||||||
border: 1px solid #ECF0F4;
|
border: 1px solid #ecf0f4;
|
||||||
margin-top: -1px;
|
margin-top: -1px;
|
||||||
margin-right: -1px;
|
margin-right: -1px;
|
||||||
&.act {
|
&.act {
|
||||||
background-color: #F7F9FA;
|
background-color: #f7f9fa;
|
||||||
}
|
}
|
||||||
.special-note-check-box {
|
.special-note-check-box {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
.check-name {
|
.check-name {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: #45576F;
|
color: #45576f;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
line-height: 1.3;
|
line-height: 1.3;
|
||||||
}
|
}
|
||||||
@ -973,7 +1027,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.calculation-estimate {
|
.calculation-estimate {
|
||||||
border: 1px solid #ECF0F4;
|
border: 1px solid #ecf0f4;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
height: 170px;
|
height: 170px;
|
||||||
@ -987,13 +1041,13 @@
|
|||||||
dt {
|
dt {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #1083E3;
|
color: #1083e3;
|
||||||
margin-bottom: 15px;
|
margin-bottom: 15px;
|
||||||
}
|
}
|
||||||
dd {
|
dd {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
color: #45576F;
|
color: #45576f;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
&:last-child {
|
&:last-child {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
@ -1025,7 +1079,7 @@
|
|||||||
.product-price-tit {
|
.product-price-tit {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
color: #45576F;
|
color: #45576f;
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
}
|
}
|
||||||
.select-wrap {
|
.select-wrap {
|
||||||
@ -1063,7 +1117,7 @@
|
|||||||
transform: translateY(-50%);
|
transform: translateY(-50%);
|
||||||
width: 1px;
|
width: 1px;
|
||||||
height: 12px;
|
height: 12px;
|
||||||
background-color: #D9D9D9;
|
background-color: #d9d9d9;
|
||||||
}
|
}
|
||||||
&:first-child {
|
&:first-child {
|
||||||
padding-left: 0;
|
padding-left: 0;
|
||||||
@ -1075,7 +1129,7 @@
|
|||||||
padding-right: 0;
|
padding-right: 0;
|
||||||
}
|
}
|
||||||
&.item01 {
|
&.item01 {
|
||||||
color: #3BBB48;
|
color: #3bbb48;
|
||||||
span {
|
span {
|
||||||
background-image: url(../../public/static/images/sub/open_ico.svg);
|
background-image: url(../../public/static/images/sub/open_ico.svg);
|
||||||
}
|
}
|
||||||
@ -1087,13 +1141,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
&.item03 {
|
&.item03 {
|
||||||
color: #0191C9;
|
color: #0191c9;
|
||||||
span {
|
span {
|
||||||
background-image: url(../../public/static/images/sub/attachment_ico.svg);
|
background-image: url(../../public/static/images/sub/attachment_ico.svg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
&.item04 {
|
&.item04 {
|
||||||
color: #F16A6A;
|
color: #f16a6a;
|
||||||
span {
|
span {
|
||||||
background-image: url(../../public/static/images/sub/click_check_ico.svg);
|
background-image: url(../../public/static/images/sub/click_check_ico.svg);
|
||||||
}
|
}
|
||||||
@ -1155,23 +1209,23 @@
|
|||||||
table {
|
table {
|
||||||
table-layout: fixed;
|
table-layout: fixed;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
border: 1px solid #ECF0F4;
|
border: 1px solid #ecf0f4;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
thead {
|
thead {
|
||||||
th {
|
th {
|
||||||
padding: 4.5px 0;
|
padding: 4.5px 0;
|
||||||
border-bottom: 1px solid #ECF0F4;
|
border-bottom: 1px solid #ecf0f4;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: #45576F;
|
color: #45576f;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
background-color: #F8F9FA;
|
background-color: #f8f9fa;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tbody {
|
tbody {
|
||||||
td {
|
td {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: #45576F;
|
color: #45576f;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 4.5px 0;
|
padding: 4.5px 0;
|
||||||
}
|
}
|
||||||
@ -1185,13 +1239,13 @@
|
|||||||
.simulation-tit-wrap {
|
.simulation-tit-wrap {
|
||||||
flex: none;
|
flex: none;
|
||||||
padding-right: 40px;
|
padding-right: 40px;
|
||||||
border-right: 1px solid #EEEEEE;
|
border-right: 1px solid #eeeeee;
|
||||||
span {
|
span {
|
||||||
display: block;
|
display: block;
|
||||||
position: relative;
|
position: relative;
|
||||||
padding-left: 60px;
|
padding-left: 60px;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
color: #14324F;
|
color: #14324f;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
&::before {
|
&::before {
|
||||||
content: '';
|
content: '';
|
||||||
@ -1219,7 +1273,7 @@
|
|||||||
}
|
}
|
||||||
dd {
|
dd {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #45576F;
|
color: #45576f;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
line-height: 24px;
|
line-height: 24px;
|
||||||
}
|
}
|
||||||
@ -1227,7 +1281,8 @@
|
|||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ul, ol{
|
ul,
|
||||||
|
ol {
|
||||||
list-style: unset;
|
list-style: unset;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1236,10 +1291,10 @@
|
|||||||
.module-total {
|
.module-total {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background-color: #F8F9FA;
|
background-color: #f8f9fa;
|
||||||
padding: 9px 0;
|
padding: 9px 0;
|
||||||
margin-right: 4px;
|
margin-right: 4px;
|
||||||
border: 1px solid #ECF0F4;
|
border: 1px solid #ecf0f4;
|
||||||
border-top: none;
|
border-top: none;
|
||||||
.total-title {
|
.total-title {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
@ -1262,7 +1317,7 @@
|
|||||||
.information-help-wrap {
|
.information-help-wrap {
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
background-color: #F4F4F4;
|
background-color: #f4f4f4;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
margin-bottom: 15px;
|
margin-bottom: 15px;
|
||||||
.information-help-tit-wrap {
|
.information-help-tit-wrap {
|
||||||
@ -1270,7 +1325,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding-right: 40px;
|
padding-right: 40px;
|
||||||
border-right: 1px solid #E0E0E3;
|
border-right: 1px solid #e0e0e3;
|
||||||
.help-tit-icon {
|
.help-tit-icon {
|
||||||
width: 40px;
|
width: 40px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
@ -1282,7 +1337,7 @@
|
|||||||
.help-tit {
|
.help-tit {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #45576F;
|
color: #45576f;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.information-help-guide {
|
.information-help-guide {
|
||||||
@ -1291,7 +1346,7 @@
|
|||||||
display: block;
|
display: block;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
color: #45576F;
|
color: #45576f;
|
||||||
margin-bottom: 7px;
|
margin-bottom: 7px;
|
||||||
&:last-child {
|
&:last-child {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
@ -1305,7 +1360,7 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 10px 0 30px 0;
|
padding: 10px 0 30px 0;
|
||||||
border-bottom: 1px solid #E5E5E5;
|
border-bottom: 1px solid #e5e5e5;
|
||||||
margin-bottom: 24px;
|
margin-bottom: 24px;
|
||||||
.community-search-box {
|
.community-search-box {
|
||||||
position: relative;
|
position: relative;
|
||||||
@ -1324,7 +1379,7 @@
|
|||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
color: #101010;
|
color: #101010;
|
||||||
&::placeholder {
|
&::placeholder {
|
||||||
color: #C8C8C8;
|
color: #c8c8c8;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.community-search-ico {
|
.community-search-ico {
|
||||||
@ -1343,10 +1398,10 @@
|
|||||||
.community-search-keyword {
|
.community-search-keyword {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
color: #45576F;
|
color: #45576f;
|
||||||
span {
|
span {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #F16A6A;
|
color: #f16a6a;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1361,15 +1416,15 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
border: 1px solid #E5E5E5;
|
border: 1px solid #e5e5e5;
|
||||||
background: #FFF;
|
background: #fff;
|
||||||
transition: all .15s ease-in-out;
|
transition: all 0.15s ease-in-out;
|
||||||
.file-item-info {
|
.file-item-info {
|
||||||
.item-num {
|
.item-num {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding: 6px 17.5px;
|
padding: 6px 17.5px;
|
||||||
border-radius: 60px;
|
border-radius: 60px;
|
||||||
background-color: #F4F4F7;
|
background-color: #f4f4f7;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #101010;
|
color: #101010;
|
||||||
@ -1401,7 +1456,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: #F4F4F7;
|
background-color: #f4f4f7;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1414,7 +1469,7 @@
|
|||||||
height: 148px;
|
height: 148px;
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
border: 1px solid #E5E5E5;
|
border: 1px solid #e5e5e5;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: #344356;
|
color: #344356;
|
||||||
@ -1426,8 +1481,8 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
width: 200px;
|
width: 200px;
|
||||||
height: 30px;
|
height: 30px;
|
||||||
background-color: #FAFAFA;
|
background-color: #fafafa;
|
||||||
border: 1px solid #EEE;
|
border: 1px solid #eee;
|
||||||
padding: 0 10px;
|
padding: 0 10px;
|
||||||
input {
|
input {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
@ -1551,5 +1606,4 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user