diff --git a/src/app/api/image/canvas/route.js b/src/app/api/image/canvas/route.js index 9f4e3f52..b96e079f 100644 --- a/src/app/api/image/canvas/route.js +++ b/src/app/api/image/canvas/route.js @@ -95,16 +95,11 @@ const resizeImage = async (image) => { const scaleY = targetImageHeight / image.bitmap.height let scale = Math.min(scaleX, scaleY) // 비율 유지하면서 최대한 크게 - // scale 저장 (나중에 전체 확대에 사용) - const originalScale = scale - let finalWidth = Math.round(image.bitmap.width * scale) let finalHeight = Math.round(image.bitmap.height * scale) - if (scale >= 0.6) { - // 실제 리사이즈 실행 - image.resize({ w: finalWidth, h: finalHeight }) - } + // 항상 리사이즈 실행 (scale >= 0.6 조건 제거) + image.resize({ w: finalWidth, h: finalHeight }) //배경 이미지를 생성 const mixedImage = new Jimp({ width: convertStandardWidth, height: convertStandardHeight, color: 0xffffffff }) @@ -119,14 +114,7 @@ const resizeImage = async (image) => { opacityDest: 1, }) - // scale이 0.8 이하인 경우 완성된 이미지를 전체적으로 확대 - if (originalScale <= 0.8) { - const enlargeRatio = 1.5 // 50% 확대 - const newWidth = Math.round(mixedImage.bitmap.width * enlargeRatio) - const newHeight = Math.round(mixedImage.bitmap.height * enlargeRatio) - - mixedImage.resize({ w: newWidth, h: newHeight }) - } + // 1.5x 확대 로직 제거 - 이미지가 템플릿 크기를 초과하지 않도록 함 return mixedImage } diff --git a/src/common/common.js b/src/common/common.js index 76632014..6ecc08fc 100644 --- a/src/common/common.js +++ b/src/common/common.js @@ -222,6 +222,9 @@ export const SAVE_KEY = [ 'skeletonLines', 'skeleton', 'viewportTransform', + 'outerLineFix', + 'adjustRoofLines', + 'northModuleYn', ] export const OBJECT_PROTOTYPE = [fabric.Line.prototype, fabric.Polygon.prototype, fabric.Triangle.prototype, fabric.Group.prototype] diff --git a/src/components/Main.jsx b/src/components/Main.jsx index 9afadca7..ad7dd3d3 100644 --- a/src/components/Main.jsx +++ b/src/components/Main.jsx @@ -18,8 +18,9 @@ import Config from '@/config/config.export' export default function MainPage() { const [sessionState, setSessionState] = useRecoilState(sessionStore) - const [chagePasswordPopOpen, setChagePasswordPopOpen] = useState(false) - + const [changePasswordPopOpen, setChangePasswordPopOpen] = useState(false) + // 데이터 확인 완료 여부 상태 추가 + const [isSessionLoaded, setIsSessionLoaded] = useState(false) const router = useRouter() const { getMessage } = useMessage() @@ -52,6 +53,14 @@ export default function MainPage() { } } + useEffect(() => { + if (isObjectNotEmpty(sessionState)) { + if (sessionState?.pwdInitYn !== 'Y') { + setChangePasswordPopOpen(true) + } + } + }, [sessionState]) + // 라디오 변경 이벤트 const handleOnChangeRadio = (e) => { setSearchRadioType(e.target.value) @@ -77,7 +86,7 @@ export default function MainPage() { useEffect(() => { if (isObjectNotEmpty(sessionState)) { if (sessionState?.pwdInitYn !== 'Y') { - setChagePasswordPopOpen(true) + setChangePasswordPopOpen(true) } } }, [sessionState]) @@ -86,10 +95,25 @@ export default function MainPage() { const [open, setOpen] = useState(false) const [modalNoticeNo, setModalNoticeNo] = useState('') + useEffect(() => { + if (isObjectNotEmpty(sessionState)) { + if (sessionState?.pwdInitYn !== 'Y') { + setChangePasswordPopOpen(true) + } else { + // pwdInitYn이 'Y'라면 팝업을 닫음 (false) + setChangePasswordPopOpen(false) + } + } + }, [sessionState]) + + //if (!isSessionLoaded) return null + return ( <> {open && } - {(!chagePasswordPopOpen && ( + {changePasswordPopOpen ? ( + + ) : ( <>
@@ -131,11 +155,8 @@ export default function MainPage() {
- )) || ( - <> - - )} + ) } diff --git a/src/components/auth/Join.jsx b/src/components/auth/Join.jsx index 4c080f00..f414cd62 100644 --- a/src/components/auth/Join.jsx +++ b/src/components/auth/Join.jsx @@ -33,6 +33,11 @@ export default function Join() { // 가입 신청 유효성 검사 const joinValidation = (formData) => { + + // 전화번호/FAX 정규식 (일본 형식: 0으로 시작, 하이픈 포함) + const telRegex = /^0\d{1,4}-\d{1,4}-\d{4}$/ + + // 판매대리점 정보 - 판매대리점명 const storeQcastNm = formData.get('storeQcastNm') if (!isObjectNotEmpty(storeQcastNm)) { @@ -65,12 +70,34 @@ export default function Join() { return false } + // 판매대리점 정보 - 전화번호 const telNo = formData.get('telNo') if (!isObjectNotEmpty(telNo)) { alert(getMessage('common.message.required.data', [getMessage('join.sub1.telNo')])) telNoRef.current.focus() return false + } else if (!telRegex.test(telNo)) { + alert(getMessage('join.validation.check1', [getMessage('join.sub1.telNo')])) + telNoRef.current.focus() + return false + } + + // + // // 판매대리점 정보 - 전화번호 + // const telNo = formData.get('telNo') + // if (!isObjectNotEmpty(telNo)) { + // alert(getMessage('common.message.required.data', [getMessage('join.sub1.telNo')])) + // telNoRef.current.focus() + // return false + // } + + // 판매대리점 정보 - FAX 번호 + const fax = formData.get('fax') + if (!isObjectNotEmpty(fax)) { + alert(getMessage('common.message.required.data', [getMessage('join.sub1.fax')])) + faxRef.current.focus() + return false } const bizNo = formData.get('bizNo') @@ -122,16 +149,38 @@ export default function Join() { } // 담당자 정보 - 전화번호 + // const userTelNo = formData.get('userTelNo') + // if (!isObjectNotEmpty(userTelNo)) { + // alert(getMessage('common.message.required.data', [getMessage('join.sub2.telNo')])) + // userTelNoRef.current.focus() + // return false + // } + + const userTelNo = formData.get('userTelNo') if (!isObjectNotEmpty(userTelNo)) { - alert(getMessage('common.message.required.data', [getMessage('join.sub2.telNo')])) + alert(getMessage('common.message.required.data', [getMessage('join.sub1.telNo')])) userTelNoRef.current.focus() return false + } else if (!telRegex.test(userTelNo)) { + alert(getMessage('join.validation.check1', [getMessage('join.sub1.telNo')])) + userTelNoRef.current.focus() + return false + } + + // 담당자 정보 - FAX 번호 + const userFax = formData.get('userFax') + if (!isObjectNotEmpty(userFax)) { + alert(getMessage('common.message.required.data', [getMessage('join.sub2.fax')])) + userFaxRef.current.focus() + return false } return true } + + // 가입 신청 const joinProcess = async (e) => { e.preventDefault() @@ -288,7 +337,8 @@ export default function Join() { name="telNo" className="input-light" maxLength={15} - onChange={inputNumberCheck} + placeholder={getMessage('join.sub1.telNo_placeholder')} + onChange={inputTelNumberCheck} ref={telNoRef} /> @@ -296,7 +346,7 @@ export default function Join() { {/* FAX 번호 */} - {getMessage('join.sub1.fax')} + {getMessage('join.sub1.fax')}*
@@ -381,7 +431,7 @@ export default function Join() {
- +
@@ -398,7 +448,8 @@ export default function Join() { name="userTelNo" className="input-light" maxLength={15} - onChange={inputNumberCheck} + placeholder={getMessage('join.sub1.telNo_placeholder')} + onChange={inputTelNumberCheck} ref={userTelNoRef} />
@@ -406,7 +457,7 @@ export default function Join() { {/* FAX 번호 */} - {getMessage('join.sub2.fax')} + {getMessage('join.sub2.fax')}*
{ + // setDisplayValue(value || '0') + // }, [value]) + useEffect(() => { - setDisplayValue(value || '0') + const newValue = value || '0' + setDisplayValue(newValue) + + // 외부에서 value가 변경될 때 계산기 내부 상태도 동기화 + const calculator = calculatorRef.current + if (calculator) { + // 연산 중이 아닐 때 외부에서 값이 들어오면 현재 피연산자로 설정 + calculator.currentOperand = newValue.toString() + calculator.previousOperand = '' + calculator.operation = undefined + setHasOperation(false) + } }, [value]) // 클릭 외부 감지 @@ -48,6 +63,33 @@ export const CalculatorInput = forwardRef( const calculator = calculatorRef.current let newDisplayValue = '' + // 블록 지정(Selection) 확인 및 처리 + if (inputRef.current) { + const { selectionStart, selectionEnd } = inputRef.current + // 텍스트 전체 또는 일부가 블록 지정된 경우 + if (selectionStart !== null && selectionEnd !== null && selectionStart !== selectionEnd) { + // 연산 중이 아닐 때만 전체 초기화 후 입력 처리 (계산기 모드 유지를 위해) + if (!hasOperation) { + calculator.currentOperand = num.toString() + calculator.previousOperand = '' + calculator.operation = undefined + calculator.shouldResetDisplay = false + + newDisplayValue = calculator.currentOperand + setDisplayValue(newDisplayValue) + onChange(newDisplayValue) + + requestAnimationFrame(() => { + if (inputRef.current) { + inputRef.current.focus() + inputRef.current.setSelectionRange(newDisplayValue.length, newDisplayValue.length) + } + }) + return // 블록 처리 로직 완료 후 종료 + } + } + } + // maxLength 체크 if (maxLength > 0) { const currentLength = (calculator.currentOperand || '').length + (calculator.previousOperand || '').length + (calculator.operation || '').length @@ -294,9 +336,11 @@ export const CalculatorInput = forwardRef( } else { calculator.currentOperand = filteredValue setHasOperation(false) + // 연산자가 없는 순수 숫자일 때만 부모 컴포넌트의 onChange 호출 + onChange(filteredValue) } - onChange(filteredValue) + //onChange(filteredValue) } } @@ -323,13 +367,19 @@ export const CalculatorInput = forwardRef( // Tab 키는 계산기 숨기고 기본 동작 허용 if (e.key === 'Tab') { + if (hasOperation) { + handleCompute(true) // 계산 수행 + } setShowKeypad(false) return } // 모든 방향키는 기본 동작 허용 if (e.key === 'ArrowLeft' || e.key === 'ArrowRight' || e.key === 'ArrowUp' || e.key === 'ArrowDown') { - setShowKeypad(true) + if (hasOperation) { + handleCompute(true) // 계산 수행 + } + setShowKeypad(false) return } @@ -343,6 +393,12 @@ export const CalculatorInput = forwardRef( return } + // --- 여기서부터는 브라우저의 기본 입력을 막고 계산기 로직만 적용함 --- + if (e.key !== 'Process') { // 한글 입력 등 특수 상황 방지 (필요시) + // e.preventDefault() 호출 위치를 확인하세요. + } + + e.preventDefault() const calculator = calculatorRef.current const { allowDecimal } = options diff --git a/src/components/common/select/QSelectBox.jsx b/src/components/common/select/QSelectBox.jsx index dbb3c285..0e77ccca 100644 --- a/src/components/common/select/QSelectBox.jsx +++ b/src/components/common/select/QSelectBox.jsx @@ -96,7 +96,7 @@ export default function QSelectBox({ title={tagTitle} >

{selected}

-
    +
      {options?.length > 0 && options?.map((option, index) => (
    • handleClickSelectOption(option)}> diff --git a/src/components/estimate/Estimate.jsx b/src/components/estimate/Estimate.jsx index a8f5f148..9107dde0 100644 --- a/src/components/estimate/Estimate.jsx +++ b/src/components/estimate/Estimate.jsx @@ -1465,19 +1465,19 @@ export default function Estimate({}) { : 'none', }} > - { - //주문분류 - setHandlePricingFlag(true) - setEstimateContextState({ estimateType: e.target.value, setEstimateContextState }) - }} - /> - + {/* {*/} + {/* //주문분류*/} + {/* setHandlePricingFlag(true)*/} + {/* setEstimateContextState({ estimateType: e.target.value, setEstimateContextState })*/} + {/* }}*/} + {/*/>*/} + {/**/}
x.itemName + ' (' + x.itemNo + ')'} + getOptionLabel={(x) => { + // 메뉴 리스트에 보이는 텍스트 디코딩 + const doc = new DOMParser().parseFromString(x.itemName, 'text/html'); + return (doc.documentElement.textContent || x.itemName) + ' (' + x.itemNo + ')'; + }} getOptionValue={(x) => x.itemNo} components={{ SingleValue: ({ children, ...props }) => { @@ -2048,13 +2052,21 @@ export default function Estimate({}) { }} isClearable={false} isDisabled={!!item?.paDispOrder} - value={displayItemList.filter(function (option) { - if (item.itemNo === '') { - return false - } else { - return option.itemId === item.itemId + value={(() => { + const selectedOption = displayItemList.find((option) => { + return item.itemNo !== '' && option.itemId === item.itemId; + }); + + if (selectedOption) { + // 현재 선택된 값의 itemName을 실시간으로 디코딩하여 전달 + const doc = new DOMParser().parseFromString(selectedOption.itemName, 'text/html'); + return { + ...selectedOption, + itemName: doc.documentElement.textContent || selectedOption.itemName + }; } - })} + return null; + })()} /> ) : ( { - handleChangeApplyParalQty(idx, serQtyIdx, e.target.value) - }} - > - {item.paralQty === 0 && ( - - )} - {Array.from( - { - length: originPcsVoltageStepUpList[index] - ? originPcsVoltageStepUpList[index]?.pcsItemList[idx].serQtyList[serQtyIdx].paralQty - : item.paralQty, - }, - (_, i) => i + 1, - ).map((num) => ( - - ))} - + stepUp?.pcsItemList.length !== 1 ? ( + + ) : ( + <>{item.paralQty} + ) ) : ( <>{item.paralQty} )} diff --git a/src/components/floor-plan/modal/circuitTrestle/step/type/PassivityCircuitAllocation.jsx b/src/components/floor-plan/modal/circuitTrestle/step/type/PassivityCircuitAllocation.jsx index 1ee73892..b3c38c8e 100644 --- a/src/components/floor-plan/modal/circuitTrestle/step/type/PassivityCircuitAllocation.jsx +++ b/src/components/floor-plan/modal/circuitTrestle/step/type/PassivityCircuitAllocation.jsx @@ -122,21 +122,36 @@ export default function PassivityCircuitAllocation(props) { return } + // targetModule중 북면 설치 여부가 Y인 것과 N인 것이 혼합이면 안됨. + const targetModuleGroup = [ + ...new Set( + canvas + .getObjects() + .filter((obj) => obj.name === POLYGON_TYPE.MODULE && targetModules.includes(obj.id)) + .map((obj) => obj.moduleInfo.northModuleYn), + ), + ] + + if (targetModuleGroup.length > 1) { + swalFire({ + text: getMessage('module.circuit.fix.not.same.roof.error'), + type: 'alert', + icon: 'warning', + }) + return + } + switch (pcsTpCd) { case 'INDFCS': { const originHaveThisPcsModules = canvas .getObjects() .filter((obj) => obj.name === POLYGON_TYPE.MODULE && obj.pcs && obj.pcs.id === selectedPcs.id) - // 이미 해당 pcs로 설치된 모듈의 surface의 방향을 구한다. - const originSurfaceList = canvas - .getObjects() - .filter((obj) => obj.name === POLYGON_TYPE.MODULE_SETUP_SURFACE && originHaveThisPcsModules.map((obj) => obj.surfaceId).includes(obj.id)) + // 1. 북면모듈, 북면외모듈 혼합 여부 체크 + const targetModuleInfos = canvas.getObjects().filter((obj) => obj.name === POLYGON_TYPE.MODULE && targetModules.includes(obj.id)) + debugger + const newTargetModuleGroup = [...new Set(targetModuleInfos.concat(originHaveThisPcsModules).map((obj) => obj.moduleInfo.northModuleYn))] - originSurfaceList.concat(originSurfaceList).forEach((surface) => { - surfaceType[`${surface.direction}-${surface.roofMaterial.pitch}`] = surface - }) - - if (Object.keys(surfaceType).length > 1) { + if (newTargetModuleGroup.length > 1) { swalFire({ text: getMessage('module.circuit.fix.not.same.roof.error'), type: 'alert', @@ -229,6 +244,7 @@ export default function PassivityCircuitAllocation(props) { roofSurface: surface.direction, roofSurfaceIncl: +canvas.getObjects().filter((obj) => obj.id === surface.parentId)[0].pitch, roofSurfaceNorthYn: surface.direction === 'north' ? 'Y' : 'N', + roofSurfaceNorthModuleYn: surface.northModuleYn, moduleList: surface.modules.map((module) => { return { itemId: module.moduleInfo.itemId, diff --git a/src/components/floor-plan/modal/lineTypes/Angle.jsx b/src/components/floor-plan/modal/lineTypes/Angle.jsx index 0faad2a4..97bbe416 100644 --- a/src/components/floor-plan/modal/lineTypes/Angle.jsx +++ b/src/components/floor-plan/modal/lineTypes/Angle.jsx @@ -2,6 +2,7 @@ import Image from 'next/image' import { useMessage } from '@/hooks/useMessage' import { normalizeDecimalLimit, normalizeDigits } from '@/util/input-utils' import { CalculatorInput } from '@/components/common/input/CalcInput' +import { useEffect } from 'react' export default function Angle({ props }) { const { getMessage } = useMessage() @@ -31,11 +32,21 @@ export default function Angle({ props }) { className="input-origin block" value={angle1} ref={angle1Ref} - onChange={(value) => setAngle1(value)} + onChange={(value) => { + // Calculate the final value first + let finalValue = value; + const numValue = parseInt(value, 10); + if (!isNaN(numValue)) { + const clampedValue = Math.min(180, Math.max(-180, numValue)); + finalValue = String(clampedValue); + } + // Set state once with the final value + setAngle1(finalValue); + }} placeholder="45" onFocus={() => (angle1Ref.current.value = '')} options={{ - allowNegative: false, + allowNegative: true, allowDecimal: true }} /> diff --git a/src/components/floor-plan/modal/lineTypes/DoublePitch.jsx b/src/components/floor-plan/modal/lineTypes/DoublePitch.jsx index 12ed66be..4919462b 100644 --- a/src/components/floor-plan/modal/lineTypes/DoublePitch.jsx +++ b/src/components/floor-plan/modal/lineTypes/DoublePitch.jsx @@ -26,17 +26,15 @@ export default function DoublePitch({ props }) { arrow2Ref, } = props - const getLength2 = () => { - const angle1Value = angle1Ref.current.value - const angle2Value = angle2Ref.current.value - const length1Value = length1Ref.current.value + const getLength2 = (angle1, angle2, length1) => { + const angle1Value = angle1 !== undefined ? angle1 : angle1Ref.current?.value + const angle2Value = angle2 !== undefined ? angle2 : angle2Ref.current?.value + const length1Value = length1 !== undefined ? length1 : length1Ref.current?.value const arrow1Value = arrow1Ref.current - const arrow2Value = arrow2Ref.current - if (angle1Value !== 0 && length1Value !== 0 && angle2Value !== 0 && arrow1Value !== '') { + if (!isNaN(Number(angle1Value)) && !isNaN(Number(length1Value)) && !isNaN(Number(angle2Value)) && arrow1Value) { const radian1 = (getDegreeByChon(angle1Value) * Math.PI) / 180 - const radian2 = (getDegreeByChon(angle2Value) * Math.PI) / 180 return Math.floor((Math.tan(radian1) * length1Value) / Math.tan(radian2)) } @@ -178,7 +176,7 @@ export default function DoublePitch({ props }) { ref={angle2Ref} onChange={(value) => { setAngle2(value) - setLength2(getLength2()) + setLength2(getLength2(angle1Ref.current?.value, value, length1Ref.current?.value)) }} placeholder="45" onFocus={() => (angle2Ref.current.value = '')} diff --git a/src/components/floor-plan/modal/lineTypes/RightAngle.jsx b/src/components/floor-plan/modal/lineTypes/RightAngle.jsx index ef2f00e2..a4356b4a 100644 --- a/src/components/floor-plan/modal/lineTypes/RightAngle.jsx +++ b/src/components/floor-plan/modal/lineTypes/RightAngle.jsx @@ -61,28 +61,40 @@ export default function RightAngle({ props }) {