1. 문제발생

리엑트로 만들어진 페이지에서 윈도우 팝업을 오픈하고, 해당 팝업 내부에서 이벤트를 통해 부모창에서 추가 팝업을 열고자 했는데 팝업이 차단되어있어 팝업이 open되지 않는 상황 >

사용자에게 해당 상황에 팝업이 차단되어있다는 사실을 고지하기 위해 부모창에서 alert을 띄웠지만 열려있는 팝업때문에 사용자에게 노출이 되지 않는 문제가 발생하였다.

 

2. 해결방법

첫번째 팝업을 ref로 관리하여 postMessage를 통해 팝업에 직접적으로 alert출력

 

3. 코드

// 첫번째팝업 ref에 할당
popElement.current = window.open('', '_blank', `width=${width},height=${height},top=${top},left=${left}`);

// 팝업 코드 할당
let element = <사용자 작성 페이지 컴포넌트 />
const htmlString = ReactDOMServer.renderToString(element);

let script = `
              <script>
                  ...
                 //클릭이벤트 postMessage (두번째팝업 오픈이벤트)
                  const info = document.querySelectorAll('.clickBtn');
                  info.forEach(el => {
                    el.addEventListener('click', function() {
                      const id = el.getAttribute('id');
                      window.opener.postMessage({ type: 'selectedItem', id }, '*');
                    });
                  });
				// alert postMessage (두번째팝업 null일경우 alert 이벤트)
                  window.addEventListener('message', function(event) {
                    if (event.origin === window.origin) {
                      alert(event.data);
                    }
                  });
                  
                </script>
                `;

      let printHtml = htmlString + script;

      popElement.current.document.writeln(printHtml);
      
  // 두번째팝업 오픈이벤트 분기
  useEffect(() => {
    const handleMessage = (event) => {
      if (event.data.type === 'selectedItem') {

        두번째팝업func(event.data.id);

      }
    };
    window.addEventListener('message', handleMessage);

    return () => {
      window.removeEventListener('message', handleMessage);
    };
  }, []);
  
  
  // 두번째팝업 오픈이벤트
  const printWindow = window.open('', '_blank', `width=${width},height=${height},top=${top},left=${left}`);
    // 두번째팝업 null일경우 예외처리
    if (!printWindow || printWindow.closed || typeof printWindow.closed === 'undefined') {
      if (popElement.current) {
        popElement.current.postMessage("Pop-ups are blocked. Please disable your pop-up blocker and try again.");
      }

      return;
    }

 

프로젝트 진행 중 서버로 param을 전송할 때에 특정 데이터의 입력 여부 확인 기능을 구현할 때 사용한 함수 내용을 정리하겠다.

 

[조건]

1. 특정데이터의 Null, 공백, List length, 특정 값 등을 Filtering 해주는 기능 구현

 

// 확인 부분. result = boolean
let param = {
	id: ...,
        pw: ...,
        addr: ...,
        name: ...,
        ..
        ..
        ..
    }

let isEmpty = isAllFieldsNullOrEmpty(param, ["addr", "name", "...", ...]/* 제외할 키 입력 */);


// null check 함수
const isAllFieldsNullOrEmpty = (param, excludeKeys = []) => {
    return Object.entries(param)
      .filter(([key]) => !excludeKeys.includes(key))  // 제외할 키들을 필터링
      .every(([key, value]) => 
        value === null || 
        value === "" || 
        value.length === 0 || /* array 데이터일 경우 */ 
        (key === 특정key && value === 특정value)
      );
  }

 

위 함수내용 기억해서 나중에 찾아 헤메지 말자....!!!!

프로젝트 진행 중 grid의 데이터 중 체크된 대상의 특정 데이터 값의 합을 구하는 기능을 구현할때 사용한 

함수 내용을 정리하겠다.

 

[조건]

1. 그리드 데이터 중 선택된 row의 count 중 특정 값이 같지 않은 데이터의 합을 구함

2. 그리드 데이터 중 선택된 row의 count를 구함

3. 그리드 데이터 중 선택된 row의 특정 데이터 값의 합(float)을 구함

 

[1]
result = checkedList.map(el => el.rowData)
		.filter((el, idx, callback) => 
        	idx === callback.findIndex(el2 => el2.id === el.id)
	).length;


[2]
result = checkedList.map(el => el.rowData)
		.reduce((total, row) => total/* 합산되는 data */ + row.invQty, 0/* total의 init value*/);


[3]
result = checkedList.map(el => el.rowData)
		.reduce((total, row) => total + parseFloat(row.amount.replace(/,/g, '')), 0);
        //replace사용은 해당 amount데이터가 자릿수 나눔이 되어 문자열 형태로 되어있기 때문

 

위 코드 활용해서 나중에 찾아헤메지 말자...!

1. Back-end 단에서 해결 (MvcConfig)

1) 작업중인 패키지 하위에 폴더를 생성하고 아래와 같은 MvcConfiguration.java 파일 생성

import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
@EnableScheduling
@EnableTransactionManagement
public class MvcConfiguration implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOrigins("http://localhost:3000")
                .allowedMethods("OPTIONS", "GET", "POST", "PUT", "DELETE");
    }
}

 

* 위의 Config 생성시 오류가 발생한다면 하기와 같은 Config 생성

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOrigins("*")
                .allowedMethods("GET", "POST")
                .maxAge(3000);
    }
}

 

 

2) Back-end단에서 api 생성

package com.study.tsxstudy_back.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class BoardController {

    @GetMapping("/apiTest")
    public String apiTest(){
        return "성공따라우쉬";
    }
    
}

 

 

3) Front-end에서 axios를 통해서 통신

useEffect(() => {
        axios.get('http://localhost:8080/apiTest')
        .then((Response)=>{
            console.log(Response.data);
        }).catch((Error)=>{
            console.log(Error);
        })
    }, [])

 

 

4) 결과확인

 

2. Back-end 단에서 해결 (Filter)

 

1) backend에 com.common 패키지 생성한후 simpleCORSFilter 파일 생성

package com.common;

import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletResponse;

public class SimpleCORSFilter implements Filter {

    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
        HttpServletResponse response = (HttpServletResponse) res;
        response.setHeader("Access-Control-Allow-Origin", "*");
        response.setHeader("Access-Control-Allow-Methods", "POST, GET, DELETE, PUT");
        response.setHeader("Access-Control-Max-Age", "3600");
        response.setHeader("Access-Control-Allow-Headers", "x-requested-with, origin, content-type, accept");
        chain.doFilter(req, res);
    }

    public void init(FilterConfig filterConfig) {}

    public void destroy() {}

}

 

2) backend단의 web.xml에 하기 코드 삽입

<!-- proxy 설정 -->
	<filter>
		<filter-name>SimpleCORSFilter</filter-name>
		<filter-class>com.common.SimpleCORSFilter</filter-class>
	</filter>

	<filter-mapping>
		<filter-name>SimpleCORSFilter</filter-name>
		<url-pattern>/*</url-pattern>
	</filter-mapping>
	<!-- proxy 설정 -->

 

 

3) pom.xml에 jackson dependency를 추가하고, dispatcher-servlet에 converterbean을 추가하여 에러해결!!!

//pom.xml
<!-- axios 통신관련 jackson dependency -->
		<dependency>
			<groupId>com.fasterxml.jackson.core</groupId>
			<artifactId>jackson-core</artifactId>
			<version>2.9.10</version>
		</dependency>
		<dependency>
			<groupId>com.fasterxml.jackson.core</groupId>
			<artifactId>jackson-databind</artifactId>
			<version>2.9.10</version>
		</dependency>
		<dependency>
			<groupId>com.fasterxml.jackson.core</groupId>
			<artifactId>jackson-annotations</artifactId>
			<version>2.9.10</version>
		</dependency>
		<dependency>
			<groupId>org.codehaus.jackson</groupId>
			<artifactId>jackson-mapper-asl</artifactId>
			<version>1.4.2</version>
		</dependency>

// dispatcher-servlet
<!-- axios 통신관련 converter -->
    <bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter">
        <property name="messageConverters">
            <list>
                <bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter" />
            </list>
        </property>
    </bean>

 

 

>>> 이외에도 Front-end 단에서 http-proxy-middleware 라이브러리를 설치하고, 아래와 같이 setProxy.js 파일을 생성하여 proxy로 해결하는 방법도 있으니 참고할 것

const { createProxyMiddleware } = require('http-proxy-middleware');

module.exports = function (app) {
  app.use(
    createProxyMiddleware('/api/v1', {
      target: 'http://localhost:8080',
      changeOrigin: true,
    })
  );
};

 

 

react.js에서 사용

  1. yarn add bootstrap 진행하여 bootstrap 설치하여 부트스트랩 이미지 적용
  2. routes.js 파일생성하여 만들어진 page의 경로와 내용을 다음과같은 형태로 작성
import Example from "./Example";
import ShippingMain from "./shipping/ShippingMain";
import ShippingManagement from "./shipping/ShippingManagement";
import ShippingRegist from "./shipping/ShippingRegist";
import StockAdjustment from "./stock/StockAdjustment";
import StockInquiry from "./stock/StockInquiry";
import StockMain from "./stock/StockMain";
import StockReceiptNPayment from "./stock/StockReceiptNPayment";
import StockTaking from "./stock/StockTaking";
import StoreManagement from "./store/StoreManagement";
import StoreRegist from "./store/StoreRegist";

const routes = [
  {
    name: "입고관리메인",
    key: "storeMain",
    route: "/storeMain",
    component: <StoreManagement />,
    breadCrumbName: "입고관리메인",
  },
  {
    name: "입고관리",
    key: "storeManagement",
    route: "/storeMain/storeManagement",
    component: <StoreManagement />,
    breadCrumbName: "입고관리",
  },
  {
    name: "입고등록",
    key: "storeRegist",
    route: "/storeMain/storeManagement/storeRegist",
    component: <StoreRegist />,
    breadCrumbName: "입고등록",
  },
  {
    name: "출고관리",
    key: "shippingMain",
    route: "/shippingMain",
    component: <ShippingMain />,
    breadCrumbName: "출고관리",
  },
  {
    name: "출고관리",
    key: "shipping",
    route: "/shippingMain/shipping",
    component: <ShippingManagement />,
    breadCrumbName: "출고관리",
  },
  {
    name: "출고등록",
    key: "shippingRegist",
    route: "/shippingMain/shipping/shippingRegist",
    component: <ShippingRegist />,
    breadCrumbName: "출고등록",
  },
  {
    name: "재고관리메인",
    key: "stockMain",
    route: "/stockMain",
    component: <StockMain />,
    breadCrumbName: "재고관리메인",
  },
  {
    name: "재고조회",
    key: "stockInquiry",
    route: "/stockMain/stockInquiry",
    component: <StockInquiry />,
    breadCrumbName: "재고조회",
  },
  {
    name: "재고실사",
    key: "stockTaking",
    route: "/stockMain/stockTaking",
    component: <StockTaking />,
    breadCrumbName: "재고실사",
  },
  {
    name: "재고조정",
    key: "stockAdjustment",
    route: "/stockMain/stockAdjustment",
    component: <StockAdjustment />,
    breadCrumbName: "재고조정",
  },
  {
    name: "수불조회",
    key: "stockReceiptNPayment",
    route: "/stockMain/stockReceiptNPayment",
    component: <StockReceiptNPayment />,
    breadCrumbName: "수불조회",
  },
  {
    name: "예시 페이지",
    key: "examplePage",
    route: "/examplePage",
    component: <Example />,
    breadCrumbName: "예시 페이지",
  },
];

export default routes;

3. 아래와같은 형식으로 url을 읽고 routes.js파일에서 받아온 요소를 토대로 경로와 표시할 데이터 설정

import React, { useEffect, useState } from "react";
import { Link, useLocation } from "react-router-dom";
import importRoutes from "./routes";

const Breadcrumbs = () => {
  const location = useLocation();
  const routePathArr = location.pathname.split("/").filter((rpt) => rpt !== ""); //경로 dept array
  const title = importRoutes
    .filter((irts) => irts.route === location.pathname)
    .map((irts) => irts.name);

  return (
    <>
      <nav aria-label="breadcrumb">
        <ol className="breadcrumb">
          {routePathArr.map((arr, idx) =>
            importRoutes
              .filter((irts) => irts.key === arr)
              .map((irts) => (
                <li key={idx} className="breadcrumb-item">
                  {routePathArr.length - 1 == idx ? (
                    <span>{irts.breadCrumbName}</span>
                  ) : (
                    <Link to={irts.route}>{irts.breadCrumbName}</Link>
                  )}
                </li>
              ))
          )}
        </ol>
      </nav>
      <h3>{title}</h3>
    </>
  );
};

export default Breadcrumbs;

4. jsx형식으로 js파일 import

return (
    <>
      <HeadLink />
      <div className="card">
        <Breadcrumbs />
        <Routes>{getRoutes(routes)}</Routes>
      </div>
    </>
  );

5. 결과확인

react에서 화면영역 프린트

  1. useRef사용하여 원하는 인쇄영역에 ref 잡아주기
const printRef = useRef();

<div ref={printRef}>
            <table style={{width:"100%", borderLeft: '#000 2px solid', lineHeight: '1.3', fontFamily: 'dotum, sans-serif', fontSize: '12px', borderTop: '#000 2px solid'}} className="tse_tbl" border={0} cellSpacing={0} cellPadding={8} width={620}>
              <tbody>
                <tr>
                  <td style={{borderBottom: '#000 1px solid', borderRight: '#000 2px solid'}} colSpan={3}>
                    <div style={{textAlign: 'center', paddingBottom: '20px', paddingLeft: '0px', paddingRight: '0px', paddingTop: '20px'}}><strong style={{fontSize: '30px'}}>휴가신청서</strong></div>
                    <table style={{lineHeight: '1.3', fontFamily: 'dotum, sans-serif', fontSize: '12px'}} className="tse_tbl" border={0} cellSpacing={0} cellPadding={0} width="100%">
                      <tbody>
                        <tr>
                                               ...
                                               ...
</div>

2. 이벤트를 통해 해당영역 인쇄 진행

<button onClick={printHandler}>인쇄하기</button>

const printHandler =()=>{
    let printContents = ReactDOM.findDOMNode(printRef.current).innerHTML;
    let windowObject = window.open('', "PrintWindow", "width=1000, height=800, top=100, left=300, toolbars=no, scrollbars=no, status=no, resizale=no");
    windowObject.document.writeln(printContents);
    windowObject.document.close();
    windowObject.focus();
    windowObject.print();
    windowObject.close();
  }

3. 결과확인

*** 추가 ***

1. 인쇄버튼 선택시 미리보기 창에서 멈추고 미리보기창에서 인쇄 또는 취소진행

2. 인쇄영역 머리글/바닥글 제거

3. 인쇄창에서 화면 전환시 자동으로 미리보기창 종료

4. 미리보기화면의 인쇄/취소 버튼 동적 추가/삭제

function App() {

  const printRef = useRef();

  const printHandler =()=>{
    const printContents = ReactDOM.findDOMNode(printRef.current).innerHTML;
    const windowObject = window.open('', "PrintWindow", "width=800, height=800, top=100, left=300, toolbars=no, scrollbars=no, status=no, resizale=no");
    windowObject.document.write(`
      <html>
        <head>
          <title>미리보기</title>
          <style>
            @media print {
              @page { 
                margin: 0; 
              }
              body {
                padding: 15mm;
              }
            }
          </style>
        </head>
        <body>
          ${printContents}
        </body>
      </html>
    `);
    windowObject.document.close();

    //버튼 동적 추가삭제 start
    const printButton = document.createElement("button");
    const cancleButton = document.createElement("button");
    printButton.innerHTML = "인쇄하기";
    cancleButton.innerHTML = "취소";
    printButton.style = "float: right; margin-bottom: 10px; margin-right: 5px";
    cancleButton.style = "float: right; margin-bottom: 10px;"

    //인쇄하기 click
    printButton.onclick =()=>{
      printButton.remove();
      cancleButton.remove();
      windowObject.print();
    }

    //취소 click
    cancleButton.onclick =()=>{
      printButton.remove();
      cancleButton.remove();
      windowObject.close();
    }

    const firstElement = windowObject.document.body.firstChild;
    windowObject.document.body.insertBefore(cancleButton, firstElement);
    windowObject.document.body.insertBefore(printButton, firstElement);

    //버튼 동적 추가삭제 end

    windowObject.focus();

    //인쇄화면에서 이동
    windowObject.onafterprint =()=>{
      printButton.remove();
      cancleButton.remove();
      windowObject.close();
    }
    
  }

  return (
    <div className="App">
        <div style={{minHeight:"800px", width:"800px"}}>
          <div style={{display:"flex", justifyContent:"center"}}>
            <Header />
          </div>
          <div style={{float:"right", marginBottom:"10px"}}>
            <button onClick={printHandler}>인쇄</button>
          </div>
          <div ref={printRef} id="printContent">
            <Print1 /> // 문서양식 component
          </div>
          <div style={{fontSize:"12px", marginTop:"5px"}}>
            <Footer />
          </div>
        </div>
    </div>
  );
}

export default App;

'react' 카테고리의 다른 글

SpringBoot - React CORS Error 해결  (0) 2023.05.24
Breadcrumb 만들기  (0) 2023.05.16
react 전체 선택/해제 구현  (0) 2023.05.16
네이버 스마트에디터(2.0) 사용하기  (0) 2023.05.16
React hooks, jsx 문법  (0) 2023.05.15
// 체크박스 이벤트
  const [checkedList, setCheckedList] = useState([]);

  // 개별체크
  const checkedInputs =(checked, item)=>{
    if(checked){
      setCheckedList([...checkedList, item]);
    } else if (!checked) {
      setCheckedList(checkedList.filter(el => el !== item));
    };
  }

  // 전체체크
  const allCheck =(checked)=>{
    if(checked){
      const idArray =[];
      users.slice(offset, offset + cntPerPage).forEach((el) => idArray.push(el.userId));
      console.log(idArray);
      setCheckedList(idArray);
    }else{
      setCheckedList([]);
    }
  }
// 전체선택
<th style={{width:"5%"}}><input type={"checkbox"} id={'checkAll'} onChange={(e) => allCheck(e.target.checked)} checked={checkedList.length === users.slice(offset, offset + cntPerPage).length ? true : false}/></th>

//개별선택
<td><input type={"checkbox"} name={'checked'+index} id={user.userId} onChange={(e)=> {checkedInputs(e.target.checked, e.target.id)}} checked={checkedList.includes(user.userId) ? true : false}/></td>

>>> 테이블에서 보여주는 개수만큼 잘라주기위해 slice를 사옹해서 전체선택을 끊어주엇다.

'react' 카테고리의 다른 글

Breadcrumb 만들기  (0) 2023.05.16
페이지 인쇄 구현  (0) 2023.05.16
네이버 스마트에디터(2.0) 사용하기  (0) 2023.05.16
React hooks, jsx 문법  (0) 2023.05.15
React 사용(in VSCode)  (0) 2023.05.15

1. 네이버 스마트에디터 github zip파일 받아온뒤 압축풀기

2. src/main/webapp 폴더하위에 smarteditor 폴더 복사

3. src 하위의 WEB-INF / jsp 폴더에 editor.jsp 생성

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> 
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<script type="text/javascript" src="smarteditor2/js/HuskyEZCreator.js" charset="utf-8"></script>
<script type="text/javascript" src="//code.jquery.com/jquery-1.11.0.min.js"></script>
<title>editorPage</title>
</head>
<body>
<div class="fr-box fr-basic fr-top" role="application">
	<textarea name="notice_content" id="smartEditor" style="width: 100%; min-height: 400px;">
		<c:forEach items="${documentList }" var="dl">
			<c:if test="${dl.id eq id}">
				${dl.formSrc } // react에서 문서양식번호를 전송받아 화면에 출력
			</c:if>
		</c:forEach>
	</textarea>
</div>
</body>

<script type="text/javascript">

	$("#smartEditor").hide(); //소스코드 출력안되게 막기
	
	var oEditors = [];
	
	nhn.husky.EZCreator.createInIFrame({
		oAppRef : oEditors,
		elPlaceHolder : "smartEditor", //저는 textarea의 id와 똑같이 적어줬습니다.
		sSkinURI : "smarteditor2/SmartEditor2Skin.html", //경로를 꼭 맞춰주세요!
		fCreator : "createSEditor2",
		htParams : {
			// 툴바 사용 여부 (true:사용/ false:사용하지 않음)
			bUseToolbar : true,
	
			// 입력창 크기 조절바 사용 여부 (true:사용/ false:사용하지 않음)
			bUseVerticalResizer : true,
	
			// 모드 탭(Editor | HTML | TEXT) 사용 여부 (true:사용/ false:사용하지 않음)
			bUseModeChanger : false
		},
	});
</script>
</html>

4. controller에서 jsp파일 매핑해주기

@Controller
public class EditorController {
	
	private static final Logger log = LoggerFactory.getLogger(EditorController.class);
	
	@Autowired
	private ApprovalService approvalService;
	
	@GetMapping("/editor.do")
	public String editorPage(@RequestParam("id") String id, Model model) {
		log.info("id   :   {}", id);
		
		List<DocumentVO> documentList = approvalService.getApprovalForm();
		model.addAttribute("id", id);
		model.addAttribute("documentList", documentList);
		
		return "editor";
	}

}

5. 리엑트에서 iframe 사용해서 jsp 호출

<tr>
  <th className="sTh">문서내용</th>
  <td colSpan={2} className="sTd">
    <select className="sInput" style={{ float:"right", width:"200px", marginBottom:"10px"}}>
      <option>선택</option>
    </select>
    <iframe
        title='smartEditor2'
        //문서양식번호 전송
        src={'http://localhost:8080/editor.do?id=' + form} 
        style={{width:"100%", height:"500px"}}  
        frameBorder={"0"}
    />
  </td>
</tr>

5. 결과확인

'react' 카테고리의 다른 글

Breadcrumb 만들기  (0) 2023.05.16
페이지 인쇄 구현  (0) 2023.05.16
react 전체 선택/해제 구현  (0) 2023.05.16
React hooks, jsx 문법  (0) 2023.05.15
React 사용(in VSCode)  (0) 2023.05.15

+ Recent posts